diff --git a/dashboard/.gitignore b/dashboard/.gitignore index 08d4cb887..c99cbb3e6 100644 --- a/dashboard/.gitignore +++ b/dashboard/.gitignore @@ -1,4 +1,6 @@ node_modules dist +yarn-error.log + !.vscode diff --git a/dashboard/biome.json b/dashboard/biome.json index 44574229e..c7e77577e 100644 --- a/dashboard/biome.json +++ b/dashboard/biome.json @@ -1,9 +1,9 @@ { - "$schema": "https://biomejs.dev/schemas/1.0.0/schema.json", + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "formatter": { "enabled": true, "indentStyle": "space", - "indentSize": 2, + "indentWidth": 2, "lineWidth": 120 }, "javascript": { @@ -16,10 +16,16 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "nursery": { + "noUnusedImports": "error" + }, + "style": { + "noNonNullAssertion": "warn" + } } }, "files": { - "ignore": ["src/api/*"] + "ignore": ["src/api/*", "package.json"] } } \ No newline at end of file diff --git a/dashboard/index.html b/dashboard/index.html index 7e2a7c1fa..404397e9a 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -4,7 +4,6 @@ - NeoShowcase diff --git a/dashboard/package.json b/dashboard/package.json index 591a29bab..fe02788d8 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -12,34 +12,41 @@ "lint:force": "biome check src --apply-unsafe", "fix": "yarn fmt:apply && yarn lint:force", "typecheck": "tsc --noEmit", - "ci": "biome ci src" + "ci": "biome ci src", + "analyze": "vite build --mode analyze" }, "license": "MIT", "devDependencies": { - "@biomejs/biome": "1.2.2", + "@biomejs/biome": "1.3.3", "@macaron-css/core": "1.2.0", "@macaron-css/solid": "1.4.1", "@macaron-css/vite": "1.4.3", - "@types/node": "20.8.0", - "typescript": "5.2.2", - "vite": "4.4.9", + "@tanstack/virtual-core": "^3.0.0-alpha.1", + "@types/node": "20.9.4", + "rollup-plugin-visualizer": "^5.9.3", + "typescript": "5.3.2", + "unplugin-fonts": "^1.0.3", + "vite": "5.0.2", "vite-plugin-compression": "0.5.1", - "vite-plugin-fonts": "0.7.0", - "vite-plugin-solid": "2.7.0", - "vite-plugin-solid-svg": "0.6.1" + "vite-plugin-solid": "2.7.2", + "vite-plugin-solid-svg": "0.7.0" }, "dependencies": { - "@bufbuild/protobuf": "1.4.1", - "@connectrpc/connect": "1.1.2", - "@connectrpc/connect-web": "1.1.2", + "@bufbuild/protobuf": "1.4.2", + "@connectrpc/connect": "1.1.3", + "@connectrpc/connect-web": "1.1.3", + "@kobalte/core": "^0.11.2", + "@modular-forms/solid": "^0.20.0", "@solid-primitives/refs": "1.0.5", - "@solidjs/router": "0.8.3", + "@solidjs/meta": "^0.29.1", + "@solidjs/router": "0.9.1", + "@tanstack/solid-virtual": "^3.0.0-beta.6", "ansi-to-html": "0.7.2", "chart.js": "4.4.0", - "fuse.js": "6.6.2", + "fuse.js": "7.0.0", "solid-chartjs": "1.3.8", - "solid-icons": "1.0.12", - "solid-js": "1.7.12", + "solid-icons": "1.1.0", + "solid-js": "1.8.6", "solid-tippy": "0.2.1", "solid-toast": "0.5.0", "tippy.js": "6.3.7" diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index f96fceb00..c9c433c9d 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,21 +1,29 @@ -import Routes from '/@/routes' +import { MetaProvider, Title } from '@solidjs/meta' import { Router } from '@solidjs/router' -import type { Component } from 'solid-js' +import { type Component, ErrorBoundary } from 'solid-js' import { Toaster } from 'solid-toast' +import Routes from '/@/routes' +import ErrorView from './components/layouts/ErrorView' +import { WithHeader } from './components/layouts/WithHeader' const App: Component = () => { return ( - <> + + NeoShowcase - + + }> + + + - + ) } diff --git a/dashboard/src/assets/icons/appState/deploying.svg b/dashboard/src/assets/icons/appState/deploying.svg new file mode 100644 index 000000000..ffa0e51ac --- /dev/null +++ b/dashboard/src/assets/icons/appState/deploying.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/dashboard/src/assets/icons/appState/error.svg b/dashboard/src/assets/icons/appState/error.svg new file mode 100644 index 000000000..69e5874dd --- /dev/null +++ b/dashboard/src/assets/icons/appState/error.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/dashboard/src/assets/icons/appState/failed.svg b/dashboard/src/assets/icons/appState/failed.svg new file mode 100644 index 000000000..a542a15f9 --- /dev/null +++ b/dashboard/src/assets/icons/appState/failed.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/dashboard/src/assets/icons/appState/idle.svg b/dashboard/src/assets/icons/appState/idle.svg new file mode 100644 index 000000000..a4bec4f58 --- /dev/null +++ b/dashboard/src/assets/icons/appState/idle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/dashboard/src/assets/icons/appState/running.svg b/dashboard/src/assets/icons/appState/running.svg new file mode 100644 index 000000000..f046f6f5a --- /dev/null +++ b/dashboard/src/assets/icons/appState/running.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/dashboard/src/assets/icons/check.svg b/dashboard/src/assets/icons/check.svg new file mode 100644 index 000000000..76d69811e --- /dev/null +++ b/dashboard/src/assets/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/dashboard/src/assets/logo.svg b/dashboard/src/assets/logo.svg new file mode 100644 index 000000000..27ff22845 --- /dev/null +++ b/dashboard/src/assets/logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/dashboard/src/assets/logo_small.svg b/dashboard/src/assets/logo_small.svg new file mode 100644 index 000000000..49c5229d8 --- /dev/null +++ b/dashboard/src/assets/logo_small.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/dashboard/src/components/AppNav.tsx b/dashboard/src/components/AppNav.tsx deleted file mode 100644 index dbcdd23c4..000000000 --- a/dashboard/src/components/AppNav.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Application, Repository } from '/@/api/neoshowcase/protobuf/gateway_pb' -import { providerToIcon, repositoryURLToProvider } from '/@/libs/application' -import { CenterInline } from '/@/libs/layout' -import { A } from '@solidjs/router' -import { JSXElement } from 'solid-js' -import { - NavAnchorActiveStyle, - NavAnchorStyle, - NavButtonsContainer, - NavContainer, - NavTitle, - NavTitleContainer, -} from './Nav' - -export interface AppNavProps { - repo: Repository - app: Application -} - -export const AppNav = (props: AppNavProps): JSXElement => { - return ( - - - {providerToIcon(repositoryURLToProvider(props.repo.url), 36)} - -
{props.repo.name}
-
/
-
{props.app.name}
-
-
- - - General - - - Builds - - - Settings - - -
- ) -} diff --git a/dashboard/src/components/AppRow.tsx b/dashboard/src/components/AppRow.tsx deleted file mode 100644 index 2e8185be2..000000000 --- a/dashboard/src/components/AppRow.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Application } from '/@/api/neoshowcase/protobuf/gateway_pb' -import { StatusIcon } from '/@/components/StatusIcon' -import { applicationState } from '/@/libs/application' -import { DiffHuman, shortSha } from '/@/libs/format' -import { vars } from '/@/theme' -import { styled } from '@macaron-css/solid' -import { A } from '@solidjs/router' -import { Component } from 'solid-js' - -const BorderContainer = styled('div', { - base: { - border: `1px solid ${vars.bg.white4}`, - selectors: { - '&:not(:last-child)': { - borderBottom: 'none', - }, - }, - }, -}) - -const ApplicationContainer = styled('div', { - base: { - display: 'grid', - gridTemplateColumns: '20px 1fr', - gap: '8px', - padding: '12px 20px', - - backgroundColor: vars.bg.white1, - }, -}) - -const AppDetail = styled('div', { - base: { - display: 'flex', - flexDirection: 'column', - gap: '4px', - }, -}) - -const AppName = styled('div', { - base: { - fontSize: '14px', - fontWeight: 500, - color: vars.text.black1, - }, -}) - -const AppFooter = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - width: '100%', - - fontSize: '11px', - color: vars.text.black3, - }, -}) - -const AppFooterRight = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - gap: '48px', - }, -}) - -export interface AppRowProps { - app: Application -} - -const AppRow: Component = (props) => { - return ( - - - - - - {props.app.name} - -
{shortSha(props.app.commit)}
- -
{props.app.websites[0]?.fqdn || ''}
- -
-
-
-
-
-
- ) -} - -export default AppRow diff --git a/dashboard/src/components/AppStatus.tsx b/dashboard/src/components/AppStatus.tsx deleted file mode 100644 index 2b901caee..000000000 --- a/dashboard/src/components/AppStatus.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Application } from '/@/api/neoshowcase/protobuf/gateway_pb' -import { StatusIcon } from '/@/components/StatusIcon' -import { ApplicationState, applicationState } from '/@/libs/application' -import { styled } from '@macaron-css/solid' -import { JSX } from 'solid-js' - -const Container = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - width: '100%', - }, -}) - -const ContainerLeft = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - gap: '8px', - alignItems: 'center', - }, -}) - -interface AppStatusProps { - apps: Application[] | undefined - state: ApplicationState -} - -export const AppStatus = (props: AppStatusProps): JSX.Element => { - const num = () => props.apps?.filter((app) => applicationState(app) === props.state)?.length ?? 0 - return ( - - - -
{props.state}
-
-
{num()}
-
- ) -} diff --git a/dashboard/src/components/AppsNew.tsx b/dashboard/src/components/AppsNew.tsx deleted file mode 100644 index 0c3b170df..000000000 --- a/dashboard/src/components/AppsNew.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { vars } from '/@/theme' -import { styled } from '@macaron-css/solid' - -export const FormButton = styled('div', { - base: { - marginLeft: '8px', - }, -}) - -export const FormTextBig = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - gap: '4px', - - fontSize: '20px', - fontWeight: 900, - color: vars.text.black1, - - marginBottom: '4px', - }, -}) - -export const FormCheckBox = styled('div', { - base: { - background: vars.bg.white1, - border: `1px solid ${vars.bg.white4}`, - borderRadius: '4px', - marginLeft: '4px', - padding: '8px 12px', - - width: '320px', - }, -}) - -export const SettingsContainer = styled('div', { - base: { - display: 'flex', - flexDirection: 'column', - gap: '10px', - }, -}) - -export const FormSettings = styled('div', { - base: { - background: vars.bg.white2, - border: `1px solid ${vars.bg.white4}`, - borderRadius: '4px', - marginLeft: '4px', - padding: '8px 12px', - - display: 'flex', - flexDirection: 'column', - gap: '12px', - }, -}) - -export const FormSettingsButton = styled('div', { - base: { - display: 'flex', - gap: '8px', - marginBottom: '4px', - }, -}) diff --git a/dashboard/src/components/ArtifactRow.tsx b/dashboard/src/components/ArtifactRow.tsx deleted file mode 100644 index 0132cdcaf..000000000 --- a/dashboard/src/components/ArtifactRow.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { Artifact } from '/@/api/neoshowcase/protobuf/gateway_pb' -import { IconButton } from '/@/components/IconButton' -import { client, handleAPIError } from '/@/libs/api' -import { DiffHuman, formatBytes } from '/@/libs/format' -import { vars } from '/@/theme' -import { styled } from '@macaron-css/solid' -import { AiOutlineDownload } from 'solid-icons/ai' -import { Component, Show } from 'solid-js' - -const BorderContainer = styled('div', { - base: { - border: `1px solid ${vars.bg.white4}`, - selectors: { - '&:not(:last-child)': { - borderBottom: 'none', - }, - }, - }, -}) - -const ApplicationContainer = styled('div', { - base: { - display: 'grid', - gridTemplateColumns: '1fr auto', - gap: '24px', - padding: '12px 20px', - alignItems: 'center', - - backgroundColor: vars.bg.white1, - }, -}) - -const AppDetail = styled('div', { - base: { - display: 'flex', - flexDirection: 'column', - gap: '4px', - }, -}) - -const AppName = styled('div', { - base: { - fontSize: '14px', - fontWeight: 500, - color: vars.text.black1, - }, -}) - -const AppFooter = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - gap: '40px', - justifyContent: 'space-between', - width: '100%', - - fontSize: '11px', - color: vars.text.black3, - }, -}) - -const AppFooterPadding = styled('div', { - base: { - width: '80px', - }, -}) - -const downloadArtifact = async (id: string) => { - try { - const data = await client.getBuildArtifact({ artifactId: id }) - const dataBlob = new Blob([data.content], { type: 'application/gzip' }) - const blobUrl = URL.createObjectURL(dataBlob) - const anchor = document.createElement('a') - anchor.href = blobUrl - anchor.download = data.filename - anchor.click() - URL.revokeObjectURL(blobUrl) - } catch (e) { - handleAPIError(e, '成果物のダウンロードに失敗しました') - } -} - -export interface ArtifactRowProps { - artifact: Artifact -} - -export const ArtifactRow: Component = (props) => { - return ( - - - - {props.artifact.name} - -
{formatBytes(+props.artifact.size.toString())}
- - -
-
- - - - } - > - downloadArtifact(props.artifact.id)}> - - - -
-
- ) -} diff --git a/dashboard/src/components/BuildConfigs.tsx b/dashboard/src/components/BuildConfigs.tsx deleted file mode 100644 index 79a14b087..000000000 --- a/dashboard/src/components/BuildConfigs.tsx +++ /dev/null @@ -1,343 +0,0 @@ -import { ApplicationConfig, RuntimeConfig, StaticConfig } from '/@/api/neoshowcase/protobuf/gateway_pb' -import { FormCheckBox, FormSettings } from '/@/components/AppsNew' -import { Checkbox } from '/@/components/Checkbox' -import { InfoTooltip } from '/@/components/InfoTooltip' -import { InputArea, InputBar, InputLabel } from '/@/components/Input' -import { RadioItem } from '/@/components/Radio' -import { Select } from '/@/components/Select' -import { PlainMessage } from '@bufbuild/protobuf' -import { Match, Switch, createEffect } from 'solid-js' -import { SetStoreFunction } from 'solid-js/store' - -export type BuildConfigMethod = ApplicationConfig['buildConfig']['case'] -const buildConfigItems: RadioItem[] = [ - { value: 'runtimeBuildpack', title: 'Runtime Buildpack' }, - { value: 'runtimeCmd', title: 'Runtime Command' }, - { value: 'runtimeDockerfile', title: 'Runtime Dockerfile' }, - { value: 'staticBuildpack', title: 'Static Buildpack' }, - { value: 'staticCmd', title: 'Static Command' }, - { value: 'staticDockerfile', title: 'Static Dockerfile' }, -] - -interface RuntimeConfigProps { - // case, valueのunionを直接使っている都合上、staticからruntimeに切り替えたときにruntimeConfigフィールドが存在しない - runtimeConfig: PlainMessage | undefined - setRuntimeConfig: >(k: K, v: PlainMessage[K]) => void -} - -const RuntimeConfigs = (props: RuntimeConfigProps) => { - return ( - <> -
- - Database - - - - props.setRuntimeConfig('useMariadb', useMariadb)} - > - MariaDB - - props.setRuntimeConfig('useMongodb', useMongodb)} - > - MongoDB - - -
-
- - Entrypoint - - - props.setRuntimeConfig('entrypoint', e.target.value)} - /> -
-
- - Command - - - props.setRuntimeConfig('command', e.target.value)} - /> -
- - ) -} - -interface StaticConfigProps { - // case, valueのunionを直接使っている都合上、runtimeからstaticに切り替えたときにstaticConfigフィールドが存在しない - staticConfig: PlainMessage | undefined - setStaticConfig: >(k: K, v: PlainMessage[K]) => void -} - -const StaticConfigs = (props: StaticConfigProps) => { - return ( - <> -
- - Artifact path - - - props.setStaticConfig('artifactPath', e.target.value)} - /> -
-
- - SPA - - - - props.setStaticConfig('spa', selected)} - > - SPA (Single Page Application) - - -
- - ) -} - -export interface BuildConfigsProps { - buildConfig: PlainMessage['buildConfig'] - setBuildConfig: SetStoreFunction['buildConfig']> -} - -export const BuildConfigs = (props: BuildConfigsProps) => { - createEffect(() => { - // @ts-ignore - if (!props.buildConfig.value.runtimeConfig) { - // @ts-ignore - props.setBuildConfig('value', 'runtimeConfig', structuredClone(new RuntimeConfig())) - } - }) - createEffect(() => { - // @ts-ignore - if (!props.buildConfig.value.staticConfig) { - // @ts-ignore - props.setBuildConfig('value', 'staticConfig', structuredClone(new StaticConfig())) - } - }) - - return ( - -
- - Type - - - { - console.log(`setting ${proto}, type: ${typeof proto}`) - props.setPort('protocol', proto) - }} - /> - - props.setPort('applicationPort', +e.target.value)} - width="tiny" - tooltip="アプリ側ポート" - /> - /{protoToName[props.port.protocol]} - - - - - - ) -} - -const suggestPort = (proto: PortPublicationProtocol): number => { - const available = systemInfo()?.ports.filter((a) => a.protocol === proto) || [] - if (available.length === 0) return 0 - const range = pickRandom(available) - return randIntN(range.endPort + 1 - range.startPort) + range.startPort -} - -const newPort = (): PlainMessage => { - return { - internetPort: suggestPort(PortPublicationProtocol.TCP), - applicationPort: 0, - protocol: PortPublicationProtocol.TCP, - } -} - -interface PortPublicationSettingsProps { - ports: PlainMessage[] - setPorts: SetStoreFunction[]> -} - -export const PortPublicationSettings = (props: PortPublicationSettingsProps) => { - return ( - - - 使用可能なポート - - - {(port) => ( -
  • - {port.startPort}/{portPublicationProtocolMap[port.protocol]}~{port.endPort}/ - {portPublicationProtocolMap[port.protocol]} -
  • - )} -
    -
    -
    - - {(port, i) => ( - props.setPorts(i(), valueName, value)} - deletePort={() => props.setPorts((current) => [...current.slice(0, i()), ...current.slice(i() + 1)])} - /> - )} - - - - - -
    - ) -} diff --git a/dashboard/src/components/Radio.tsx b/dashboard/src/components/Radio.tsx deleted file mode 100644 index 2c13b110e..000000000 --- a/dashboard/src/components/Radio.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { vars } from '/@/theme' -import { styled } from '@macaron-css/solid' -import { ImRadioChecked, ImRadioUnchecked } from 'solid-icons/im' -import { For, JSXElement } from 'solid-js' - -const Container = styled('div', { - base: { - display: 'flex', - flexDirection: 'column', - gap: '12px', - - fontSize: '20px', - color: vars.text.black1, - }, -}) - -const ItemContainer = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - gap: '12px', - - cursor: 'pointer', - alignItems: 'center', - }, -}) - -export interface RadioItem { - value: T - title: string -} - -export interface Props { - items: RadioItem[] - selected: T - setSelected: (s: T) => void - onClick?: () => void -} - -export const Radio = (props: Props): JSXElement => { - return ( - - - {(item) => ( - { - props.setSelected(item.value) - props.onClick?.() - }} - > - {props.selected === item.value ? ( - - ) : ( - - )} - {item.title} - - )} - - - ) -} diff --git a/dashboard/src/components/RepositoryAuthSettings.tsx b/dashboard/src/components/RepositoryAuthSettings.tsx deleted file mode 100644 index 2f2d7f527..000000000 --- a/dashboard/src/components/RepositoryAuthSettings.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { InfoTooltip } from '/@/components/InfoTooltip' -import { PlainMessage } from '@bufbuild/protobuf' -import { styled } from '@macaron-css/solid' -import { OcCopy2 } from 'solid-icons/oc' -import { Component, Match, Show, Switch, createEffect, createResource, createSignal } from 'solid-js' -import { SetStoreFunction } from 'solid-js/store' -import { CreateRepositoryAuth } from '../api/neoshowcase/protobuf/gateway_pb' -import { client, systemInfo } from '../libs/api' -import { writeToClipboard } from '../libs/clipboard' -import { vars } from '../theme' -import { Button } from './Button' -import { IconButton } from './IconButton' -import { InputBar, InputLabel } from './Input' -import { Radio } from './Radio' - -const SshDetails = styled('div', { - base: { - color: vars.text.black2, - marginBottom: '4px', - }, -}) - -const PublicKeyCode = styled('code', { - base: { - display: 'block', - padding: '8px 12px', - fontSize: '14px', - background: vars.bg.white2, - color: vars.text.black1, - border: `1px solid ${vars.bg.white4}`, - borderRadius: '4px', - }, -}) - -const Row = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - gap: '8px', - alignItems: 'center', - }, -}) - -interface RepositoryAuthSettingsProps { - authConfig: PlainMessage - setAuthConfig: SetStoreFunction> -} - -export const RepositoryAuthSettings: Component = (props) => { - const [useTmpKey, setUseTmpKey] = createSignal(false) - const [tmpKey] = createResource( - () => (useTmpKey() ? true : undefined), - () => client.generateKeyPair({}), - ) - createEffect(() => { - if (!tmpKey()) return - props.setAuthConfig('auth', 'value', { keyId: tmpKey()?.keyId }) - }) - const publicKey = () => (useTmpKey() ? tmpKey()?.publicKey : systemInfo()?.publicKey) - - const handleCopyPublicKey = () => writeToClipboard(publicKey()) - - return ( -
    - 認証方法 - props.setAuthConfig('auth', 'case', v)} - /> - - - {(v) => ( - <> - ユーザー名 - - props.setAuthConfig('auth', 'value', { - username: e.currentTarget.value, - }) - } - /> - パスワード - - props.setAuthConfig('auth', 'value', { - password: e.currentTarget.value, - }) - } - /> - - )} - - - - 以下のSSH公開鍵{!useTmpKey() && ' (システム共通) '} - をリポジトリに登録してください。 - - - {publicKey()} - - - - - - - - - - - - -
    - ) -} diff --git a/dashboard/src/components/RepositoryInfo.tsx b/dashboard/src/components/RepositoryInfo.tsx deleted file mode 100644 index 01606b552..000000000 --- a/dashboard/src/components/RepositoryInfo.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Repository } from '/@/api/neoshowcase/protobuf/gateway_pb' -import { providerToIcon, repositoryURLToProvider } from '/@/libs/application' -import { vars } from '/@/theme' -import { styled } from '@macaron-css/solid' -import { JSXElement } from 'solid-js' - -const RepositoryInfoContainer = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - gap: '10px', - }, -}) - -const RepoName = styled('div', { - base: { - fontSize: '16px', - fontWeight: 500, - color: vars.text.black1, - }, -}) - -const RepositoryInfoBackground = styled('div', { - base: { - display: 'flex', - - background: vars.bg.white3, - border: `1px solid ${vars.bg.white4}`, - borderRadius: '4px', - padding: '8px 12px', - }, -}) - -const SmallText = styled('div', { - base: { - display: 'flex', - fontSize: '11px', - color: vars.text.black3, - }, -}) - -export interface RepositoryInfoProps { - repo: Repository -} - -export const RepositoryInfo = (props: RepositoryInfoProps): JSXElement => { - const provider = repositoryURLToProvider(props.repo.url) - return ( - - - {providerToIcon(provider)} - {props.repo.name} - {props.repo.url} - - - ) -} diff --git a/dashboard/src/components/RepositoryNav.tsx b/dashboard/src/components/RepositoryNav.tsx deleted file mode 100644 index 314a70b7b..000000000 --- a/dashboard/src/components/RepositoryNav.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Repository } from '/@/api/neoshowcase/protobuf/gateway_pb' -import { A } from '@solidjs/router' -import { Component } from 'solid-js' -import { providerToIcon, repositoryURLToProvider } from '../libs/application' -import { CenterInline } from '../libs/layout' -import { NavAnchorActiveStyle, NavAnchorStyle, NavButtonsContainer, NavContainer, NavTitleContainer } from './Nav' - -export interface RepositoryNavProps { - repository: Repository -} - -const RepositoryNav: Component = (props) => { - return ( - - - {providerToIcon(repositoryURLToProvider(props.repository.url), 36)} - {props.repository.name} - - - - General - - - Settings - - - - ) -} - -export default RepositoryNav diff --git a/dashboard/src/components/RepositoryRow.tsx b/dashboard/src/components/RepositoryRow.tsx deleted file mode 100644 index 6de45c68e..000000000 --- a/dashboard/src/components/RepositoryRow.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Application, Repository } from '/@/api/neoshowcase/protobuf/gateway_pb' -import AppRow from '/@/components/AppRow' -import { providerToIcon, repositoryURLToProvider } from '/@/libs/application' -import { vars } from '/@/theme' -import { styled } from '@macaron-css/solid' -import { A } from '@solidjs/router' -import { For, JSXElement } from 'solid-js' - -const Header = styled('div', { - base: { - height: '60px', - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - - padding: '0 20px', - backgroundColor: vars.bg.white3, - - borderRadius: '4px', - border: `1px solid ${vars.bg.white4}`, - overflow: 'hidden', - - selectors: { - '&:not(:last-child)': { - borderBottom: 'none', - borderRadius: '4px 4px 0 0', - }, - }, - }, -}) - -const HeaderLeft = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - gap: '8px', - width: '100%', - }, -}) - -const RepoName = styled('div', { - base: { - fontSize: '16px', - fontWeight: 500, - color: vars.text.black1, - }, -}) - -const AppsCount = styled('div', { - base: { - display: 'flex', - fontSize: '11px', - color: vars.text.black3, - }, -}) - -const AddBranchButton = styled('div', { - base: { - display: 'flex', - alignItems: 'center', - - padding: '8px 16px', - borderRadius: '4px', - backgroundColor: vars.bg.white5, - - fontSize: '12px', - color: vars.text.black2, - }, -}) - -export type Provider = 'GitHub' | 'GitLab' | 'Gitea' - -export interface Props { - repo: Repository - apps: Application[] -} - -export const RepositoryRow = (props: Props): JSXElement => { - const provider = repositoryURLToProvider(props.repo.url) - return ( - - ) -} diff --git a/dashboard/src/components/Select.tsx b/dashboard/src/components/Select.tsx deleted file mode 100644 index 47305b655..000000000 --- a/dashboard/src/components/Select.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { vars } from '/@/theme' -import { styled } from '@macaron-css/solid' -import { For } from 'solid-js' - -const StyledSelect = styled('select', { - base: { - display: 'flex', - flexDirection: 'column', - gap: '2px', - - padding: '8px 12px', - borderRadius: '4px', - border: `1px solid ${vars.bg.white4}`, - fontSize: '14px', - marginLeft: '4px', - - selectors: { - '&:focus': { - border: `1px solid ${vars.bg.black1}`, - }, - }, - }, -}) - -export interface SelectItem { - value: T - title: string -} - -export interface SelectProps { - items: SelectItem[] - selected: T - onSelect: (s: T) => void -} - -export const Select = (props: SelectProps) => { - return ( - props.onSelect(props.items[e.target.selectedIndex].value)}> - {(item) => } - - ) -} diff --git a/dashboard/src/components/StatusIcon.tsx b/dashboard/src/components/StatusIcon.tsx deleted file mode 100644 index 3e398adf0..000000000 --- a/dashboard/src/components/StatusIcon.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ApplicationState } from '/@/libs/application' -import { vars } from '/@/theme' -import { AiFillCheckCircle, AiFillExclamationCircle, AiFillMinusCircle } from 'solid-icons/ai' -import { IoReloadCircle } from 'solid-icons/io' -import { JSXElement } from 'solid-js' -import { Dynamic } from 'solid-js/web' - -interface IconProps { - size: number -} -const components: Record JSXElement> = { - Idle: (props) => , - Deploying: (props) => , - Running: (props) => , - Static: (props) => , - Error: (props) => , -} - -interface Props { - state: ApplicationState - size?: number -} - -export const StatusIcon = (props: Props): JSXElement => { - return -} diff --git a/dashboard/src/components/UI/Badge.tsx b/dashboard/src/components/UI/Badge.tsx new file mode 100644 index 000000000..20a17e7ec --- /dev/null +++ b/dashboard/src/components/UI/Badge.tsx @@ -0,0 +1,30 @@ +import { styled } from '@macaron-css/solid' +import { colorVars, textVars } from '/@/theme' + +const Badge = styled('div', { + base: { + height: '1.43em', // 20px + padding: '0 8px', + borderRadius: '9999px', + + ...textVars.caption.regular, + }, + variants: { + variant: { + text: { + background: colorVars.primitive.blackAlpha[200], + color: colorVars.semantic.text.black, + }, + success: { + background: colorVars.semantic.transparent.successHover, + color: colorVars.semantic.accent.success, + }, + warn: { + background: colorVars.semantic.transparent.warnHover, + color: colorVars.semantic.accent.warn, + }, + }, + }, +}) + +export default Badge diff --git a/dashboard/src/components/UI/Button.tsx b/dashboard/src/components/UI/Button.tsx new file mode 100644 index 000000000..84b6fc29a --- /dev/null +++ b/dashboard/src/components/UI/Button.tsx @@ -0,0 +1,228 @@ +import { styled } from '@macaron-css/solid' +import { JSX, ParentComponent, Show, splitProps } from 'solid-js' +import { colorOverlay } from '/@/libs/colorOverlay' +import { colorVars, textVars } from '/@/theme' +import { ToolTip, TooltipProps } from './ToolTip' + +const Container = styled('button', { + base: { + width: 'auto', + display: 'flex', + alignItems: 'center', + borderRadius: '8px', + gap: '4px', + + background: 'none', + border: 'none', + cursor: 'pointer', + selectors: { + '&:disabled': { + cursor: 'not-allowed', + border: 'none !important', + color: `${colorVars.semantic.text.black} !important`, + background: `${colorVars.semantic.text.disabled} !important`, + }, + '&[data-loading="true"]': { + cursor: 'wait', + border: 'none !important', + color: `${colorVars.semantic.text.black} !important`, + background: `${colorVars.semantic.text.disabled} !important`, + }, + }, + }, + variants: { + size: { + medium: { + height: '44px', + padding: '0 16px', + }, + small: { + height: '32px', + padding: '0 12px', + }, + }, + full: { + true: { + width: '100%', + }, + }, + variants: { + primary: { + background: colorVars.semantic.primary.main, + color: colorVars.semantic.text.white, + selectors: { + '&:hover': { + background: colorOverlay(colorVars.semantic.primary.main, colorVars.primitive.blackAlpha[200]), + }, + '&:active, &[data-active="true"]': { + background: colorOverlay(colorVars.semantic.primary.main, colorVars.primitive.blackAlpha[300]), + }, + }, + }, + ghost: { + background: colorVars.semantic.ui.secondary, + color: colorVars.semantic.text.black, + selectors: { + '&:hover': { + background: colorOverlay(colorVars.semantic.ui.secondary, colorVars.primitive.blackAlpha[50]), + }, + '&:active, &[data-active="true"]': { + background: colorOverlay(colorVars.semantic.ui.secondary, colorVars.primitive.blackAlpha[200]), + }, + }, + }, + border: { + border: `solid 1px ${colorVars.semantic.ui.border}`, + color: colorVars.semantic.text.black, + selectors: { + '&:hover': { + background: colorVars.semantic.transparent.primaryHover, + }, + '&:active, &[data-active="true"]': { + background: colorVars.semantic.transparent.primarySelected, + }, + }, + }, + text: { + color: colorVars.semantic.text.black, + selectors: { + '&:hover': { + background: colorVars.semantic.transparent.primaryHover, + }, + '&:active, &[data-active="true"]': { + color: colorVars.semantic.primary.main, + background: colorVars.semantic.transparent.primarySelected, + }, + }, + }, + primaryError: { + border: `solid 1px ${colorVars.semantic.accent.error}`, + background: colorVars.semantic.accent.error, + color: colorVars.semantic.text.white, + selectors: { + '&:hover': { + background: colorOverlay(colorVars.semantic.accent.error, colorVars.primitive.blackAlpha[200]), + }, + '&:active, &[data-active="true"]': { + background: colorOverlay(colorVars.semantic.accent.error, colorVars.primitive.blackAlpha[300]), + }, + }, + }, + borderError: { + border: `solid 1px ${colorVars.semantic.accent.error}`, + color: colorVars.semantic.accent.error, + selectors: { + '&:hover': { + background: colorVars.semantic.transparent.errorHover, + }, + '&:active, &[data-active="true"]': { + background: colorVars.semantic.transparent.errorSelected, + }, + }, + }, + textError: { + color: colorVars.semantic.accent.error, + selectors: { + '&:hover': { + background: colorVars.semantic.transparent.errorHover, + }, + '&:active, &[data-active="true"]': { + background: colorVars.semantic.transparent.errorSelected, + }, + }, + }, + }, + hasCheckbox: { + true: { + gap: '8px', + }, + }, + }, +}) +const Text = styled('div', { + base: { + whiteSpace: 'nowrap', + }, + variants: { + size: { + medium: { + ...textVars.text.bold, + }, + small: { + ...textVars.caption.bold, + }, + }, + }, +}) +const IconContainer = styled('div', { + base: { + lineHeight: 1, + }, + variants: { + size: { + medium: { + width: '24px', + height: '24px', + }, + small: { + width: '20px', + height: '20px', + }, + }, + }, +}) + +export interface Props extends JSX.ButtonHTMLAttributes { + variants: 'primary' | 'ghost' | 'border' | 'text' | 'primaryError' | 'borderError' | 'textError' + size: 'medium' | 'small' + loading?: boolean + active?: boolean + hasCheckbox?: boolean + full?: boolean + tooltip?: TooltipProps + leftIcon?: JSX.Element + rightIcon?: JSX.Element +} + +export const Button: ParentComponent = (props) => { + const [addedProps, originalButtonProps] = splitProps(props, [ + 'variants', + 'size', + 'loading', + 'active', + 'hasCheckbox', + 'full', + 'tooltip', + 'leftIcon', + 'rightIcon', + 'children', + ]) + + return ( + + + + + {addedProps.leftIcon} + + {addedProps.children} + + {addedProps.rightIcon} + + + + + ) +} diff --git a/dashboard/src/components/UI/CheckBoxIcon.tsx b/dashboard/src/components/UI/CheckBoxIcon.tsx new file mode 100644 index 000000000..889d4a085 --- /dev/null +++ b/dashboard/src/components/UI/CheckBoxIcon.tsx @@ -0,0 +1,67 @@ +import { styled } from '@macaron-css/solid' +import { Component, Show } from 'solid-js' +import CheckMark from '/@/assets/icons/check.svg' +// import { colorOverlay } from '/@/libs/colorOverlay' +import { colorVars } from '/@/theme' + +const Container = styled('div', { + base: { + width: '100%', + height: 'auto', + aspectRatio: '1', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + borderRadius: '4px', + color: colorVars.semantic.ui.primary, + }, + variants: { + checked: { + false: { + background: colorVars.semantic.ui.background, + border: `2px solid ${colorVars.semantic.ui.tertiary}`, + // selectors: { + // '&:hover': { + // background: colorOverlay(colorVars.semantic.ui.tertiary, colorVars.primitive.blackAlpha[200]), + // }, + // '&:active': { + // background: colorOverlay(colorVars.semantic.ui.tertiary, colorVars.primitive.blackAlpha[300]), + // }, + // }, + }, + true: { + background: colorVars.semantic.primary.main, + // selectors: { + // '&:hover': { + // background: colorOverlay(colorVars.semantic.primary.main, colorVars.primitive.blackAlpha[200]), + // }, + // '&:active': { + // background: colorOverlay(colorVars.semantic.primary.main, colorVars.primitive.blackAlpha[300]), + // }, + // }, + }, + }, + disabled: { + true: { + cursor: 'not-allowed', + background: `${colorVars.semantic.text.disabled} !important`, + }, + }, + }, +}) + +export interface Props { + checked: boolean + disabled?: boolean +} + +export const CheckBoxIcon: Component = (props) => { + return ( + + + + + + ) +} diff --git a/dashboard/src/components/UI/Code.tsx b/dashboard/src/components/UI/Code.tsx new file mode 100644 index 000000000..90442f57c --- /dev/null +++ b/dashboard/src/components/UI/Code.tsx @@ -0,0 +1,87 @@ +import { styled } from '@macaron-css/solid' +import { Component, Show } from 'solid-js' +import { writeToClipboard } from '/@/libs/clipboard' +import { colorVars } from '/@/theme' +import { MaterialSymbols } from './MaterialSymbols' +import { ToolTip } from './ToolTip' + +const Container = styled('div', { + base: { + position: 'relative', + width: '100%', + minHeight: 'calc(1lh + 8px)', + marginTop: '4px', + whiteSpace: 'pre-wrap', + overflowX: 'auto', + padding: '4px 8px', + fontSize: '16px', + lineHeight: '1.5', + fontFamily: 'Menlo, Monaco, Consolas, Courier New, monospace !important', + background: colorVars.semantic.ui.secondary, + borderRadius: '4px', + color: colorVars.semantic.text.black, + }, + variants: { + copyable: { + true: { + paddingRight: '40px', + }, + }, + }, +}) +const CopyButton = styled('button', { + base: { + position: 'absolute', + width: '24px', + height: '24px', + top: '4px', + right: '8px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + border: `solid 1px ${colorVars.semantic.ui.border}`, + borderRadius: '4px', + + cursor: 'pointer', + color: colorVars.semantic.text.black, + background: 'none', + lineHeight: 1, + + selectors: { + '&:hover': { + background: colorVars.primitive.blackAlpha[200], + }, + '&:active': { + background: colorVars.primitive.blackAlpha[300], + }, + }, + }, +}) + +const Code: Component<{ + value: string + copyable?: boolean +}> = (props) => { + const handleCopy = () => { + writeToClipboard(props.value) + } + + return ( + + {props.value} + + + + content_copy + + + + + ) +} + +export default Code diff --git a/dashboard/src/components/UI/JumpButton.tsx b/dashboard/src/components/UI/JumpButton.tsx new file mode 100644 index 000000000..38820262d --- /dev/null +++ b/dashboard/src/components/UI/JumpButton.tsx @@ -0,0 +1,46 @@ +import { styled } from '@macaron-css/solid' +import { A } from '@solidjs/router' +import { VoidComponent } from 'solid-js' +import { colorVars } from '/@/theme' +import { MaterialSymbols } from './MaterialSymbols' + +const JumpButtonContainer = styled('div', { + base: { + width: '32px', + height: '32px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + flexShrink: 0, + background: 'none', + border: 'none', + borderRadius: '6px', + cursor: 'pointer', + color: colorVars.semantic.text.black, + selectors: { + '&:hover': { + background: colorVars.semantic.transparent.primaryHover, + }, + '&:active, &[data-active="true"]': { + color: colorVars.semantic.primary.main, + background: colorVars.semantic.transparent.primarySelected, + }, + '&:disabled': { + cursor: 'not-allowed', + border: 'none !important', + color: `${colorVars.semantic.text.black} !important`, + background: `${colorVars.semantic.text.disabled} !important`, + }, + }, + }, +}) +const JumpButton: VoidComponent<{ href: string }> = (props) => ( + + + arrow_outward + + +) + +export default JumpButton diff --git a/dashboard/src/components/Log.tsx b/dashboard/src/components/UI/LogContainer.tsx similarity index 74% rename from dashboard/src/components/Log.tsx rename to dashboard/src/components/UI/LogContainer.tsx index 5efc34736..d8fd77210 100644 --- a/dashboard/src/components/Log.tsx +++ b/dashboard/src/components/UI/LogContainer.tsx @@ -1,16 +1,16 @@ -import { vars } from '/@/theme' import { styled } from '@macaron-css/solid' +import { colorVars } from '/@/theme' export const LogContainer = styled('code', { base: { display: 'flex', flexDirection: 'column', fontSize: '15px', - lineHeight: '20px', + lineHeight: '150%', - backgroundColor: vars.bg.black1, - padding: '10px', - color: vars.text.white1, + backgroundColor: colorVars.primitive.gray[800], + padding: '4px 8px', + color: colorVars.semantic.text.white, borderRadius: '4px', maxHeight: '500px', diff --git a/dashboard/src/components/UI/MaterialSymbols.tsx b/dashboard/src/components/UI/MaterialSymbols.tsx new file mode 100644 index 000000000..833a73ce2 --- /dev/null +++ b/dashboard/src/components/UI/MaterialSymbols.tsx @@ -0,0 +1,71 @@ +import { style } from '@macaron-css/core' +import { JSX, ParentComponent, mergeProps, splitProps } from 'solid-js' + +// see https://developers.google.com/fonts/docs/material_symbols?hl=ja#self-hosting_the_font +const baseStyle = style({ + fontFamily: 'Material Symbols Rounded', + fontWeight: 'normal', + fontStyle: 'normal', + display: 'inline-block', + lineHeight: 1, + textTransform: 'none', + letterSpacing: 'normal', + wordWrap: 'normal', + whiteSpace: 'nowrap', + direction: 'ltr', + flexShrink: 0, + overflow: 'hidden', +}) + +export interface Props extends JSX.HTMLAttributes { + fill?: boolean + weight?: 300 + grade?: 0 + opticalSize?: 20 | 24 + displaySize?: number + color?: string +} + +export const MaterialSymbols: ParentComponent = (props) => { + const [addedProps, originalProps] = splitProps(props, [ + 'children', + 'fill', + 'weight', + 'grade', + 'opticalSize', + 'displaySize', + 'color', + ]) + const mergedProps = mergeProps( + { + type: 'rounded', + fill: false, + weight: 300, + grade: 0, + opticalSize: 24, + color: 'currentColor', + }, + addedProps, + ) + const size = () => (mergedProps.displaySize ? `${mergedProps.displaySize}px` : `${mergedProps.opticalSize}px`) + + return ( + + {mergedProps.children} + + ) +} diff --git a/dashboard/src/components/UI/ModalDeleteConfirm.tsx b/dashboard/src/components/UI/ModalDeleteConfirm.tsx new file mode 100644 index 000000000..6d07046e7 --- /dev/null +++ b/dashboard/src/components/UI/ModalDeleteConfirm.tsx @@ -0,0 +1,24 @@ +import { styled } from '@macaron-css/solid' +import { ParentComponent } from 'solid-js' +import { colorVars, textVars } from '/@/theme' + +const DeleteConfirm = styled('div', { + base: { + width: '100%', + padding: '16px 20px', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: '8px', + borderRadius: '8px', + background: colorVars.semantic.ui.secondary, + overflowWrap: 'anywhere', + color: colorVars.semantic.text.black, + ...textVars.h3.regular, + }, +}) +const ModalDeleteConfirm: ParentComponent = (props) => { + return {props.children} +} + +export default ModalDeleteConfirm diff --git a/dashboard/src/components/UI/RadioIcon.tsx b/dashboard/src/components/UI/RadioIcon.tsx new file mode 100644 index 000000000..1b2738c63 --- /dev/null +++ b/dashboard/src/components/UI/RadioIcon.tsx @@ -0,0 +1,79 @@ +import { styled } from '@macaron-css/solid' +import { Component, Show } from 'solid-js' +// import { colorOverlay } from '/@/libs/colorOverlay' +import { colorVars } from '/@/theme' + +const Container = styled('div', { + base: { + width: '20px', + height: '20px', + + borderRadius: '9999px', + color: colorVars.semantic.ui.primary, + }, + variants: { + checked: { + false: { + background: colorVars.semantic.ui.background, + border: `2px solid ${colorVars.semantic.ui.tertiary}`, + // selectors: { + // '&:hover': { + // background: colorOverlay(colorVars.semantic.ui.tertiary, colorVars.primitive.blackAlpha[200]), + // }, + // '&:active': { + // background: colorOverlay(colorVars.semantic.ui.tertiary, colorVars.primitive.blackAlpha[300]), + // }, + // }, + }, + true: { + background: colorVars.semantic.primary.main, + selectors: { + '&::after': { + // white circle in the middle + content: '""', + width: '8px', + height: '8px', + borderRadius: '9999px', + background: colorVars.semantic.ui.primary, + }, + // '&:hover': { + // background: colorOverlay(colorVars.semantic.primary.main, colorVars.primitive.blackAlpha[200]), + // }, + // '&:active': { + // background: colorOverlay(colorVars.semantic.primary.main, colorVars.primitive.blackAlpha[300]), + // }, + }, + }, + }, + disabled: { + true: { + cursor: 'not-allowed', + background: `${colorVars.semantic.text.disabled} !important`, + }, + }, + }, +}) + +export interface Props { + selected: boolean + disabled?: boolean +} + +export const RadioIcon: Component = (props) => { + return ( + + + + + + + + ) +} diff --git a/dashboard/src/components/UI/Skeleton.tsx b/dashboard/src/components/UI/Skeleton.tsx new file mode 100644 index 000000000..759ab3aaf --- /dev/null +++ b/dashboard/src/components/UI/Skeleton.tsx @@ -0,0 +1,59 @@ +import { Skeleton as KSkeleton } from '@kobalte/core' +import { SkeletonProps } from '@kobalte/core/dist/types/skeleton/skeleton' +import { keyframes, style } from '@macaron-css/core' +import { Component, mergeProps } from 'solid-js' + +const skeletonAnimation = keyframes({ + from: { + transform: 'translateX(-100%)', + }, + to: { + transform: 'translateX(100%)', + }, +}) +const skeletonClass = style({ + position: 'relative', + width: 'auto', + height: 'auto', + flexShrink: '0', + opacity: '0.2', + + selectors: { + "&[data-visible='true']": { + overflow: 'hidden', + }, + "&[data-visible='true']::after": { + position: 'absolute', + content: '""', + inset: '0', + backgroundColor: 'currentcolor', + backgroundImage: 'linear-gradient(90deg, transparent, #fff4, transparent)', + }, + "&[data-visible='true']::before": { + position: 'absolute', + content: '""', + inset: '0', + backgroundColor: 'currentcolor', + }, + "&[data-animate='true']::after": { + animation: `${skeletonAnimation} 1.5s linear infinite`, + }, + }, +}) + +const defaultProps: SkeletonProps = { + radius: 999, + width: -1, // for `width: auto` +} + +const Skeleton: Component = (props) => { + const mergedProps = mergeProps(defaultProps, props) + + return ( + + {props.children} + + ) +} + +export default Skeleton diff --git a/dashboard/src/components/UI/StepProgress.tsx b/dashboard/src/components/UI/StepProgress.tsx new file mode 100644 index 000000000..d2a9cdc52 --- /dev/null +++ b/dashboard/src/components/UI/StepProgress.tsx @@ -0,0 +1,98 @@ +import { styled } from '@macaron-css/solid' +import { Component } from 'solid-js' +import { colorVars, textVars } from '/@/theme' + +const Steps = styled('div', { + base: { + width: '100%', + display: 'flex', + flexDirection: 'row', + gap: '16px', + }, +}) +const Container = styled('div', { + base: { + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: '8px', + }, +}) +const Bar = styled('div', { + base: { + width: '100%', + height: '4px', + borderRadius: '2px', + }, + variants: { + state: { + complete: { + background: colorVars.semantic.primary.main, + }, + current: { + background: colorVars.semantic.primary.main, + }, + incomplete: { + background: colorVars.semantic.ui.tertiary, + }, + }, + }, +}) +const Content = styled('div', { + base: { + width: '100%', + padding: '6px 12px 8px 12px', + display: 'flex', + flexDirection: 'column', + borderRadius: '4px', + }, + variants: { + state: { + complete: { + color: colorVars.semantic.primary.main, + }, + current: { + color: colorVars.semantic.primary.main, + background: colorVars.semantic.transparent.primaryHover, + }, + incomplete: { + color: colorVars.semantic.text.grey, + }, + }, + }, +}) +const Title = styled('div', { + base: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: '6px', + ...textVars.h3.bold, + }, +}) +const Description = styled('div', { + base: { + ...textVars.caption.regular, + }, +}) + +const StepProgress: Component<{ + title: string + description: string + state: 'complete' | 'current' | 'incomplete' +}> = (props) => { + return ( + + + + {props.title} + {props.description} + + + ) +} + +export const Progress = { + Container: Steps, + Step: StepProgress, +} diff --git a/dashboard/src/components/UI/TabRound.tsx b/dashboard/src/components/UI/TabRound.tsx new file mode 100644 index 000000000..515dacace --- /dev/null +++ b/dashboard/src/components/UI/TabRound.tsx @@ -0,0 +1,72 @@ +import { styled } from '@macaron-css/solid' +import { JSX, splitProps } from 'solid-js' +import { ParentComponent } from 'solid-js' +import { colorVars, textVars } from '/@/theme' + +const Container = styled('button', { + base: { + width: 'fit-content', + height: '44px', + padding: '0 16px', + display: 'flex', + alignItems: 'center', + gap: '4px', + border: 'none', + borderRadius: '9999px', + ...textVars.h4.medium, + whiteSpace: 'nowrap', + cursor: 'pointer', + }, + variants: { + state: { + active: { + background: colorVars.semantic.transparent.primaryHover, + color: `${colorVars.semantic.primary.main} !important`, + boxShadow: `inset 0 0 0 2px ${colorVars.semantic.primary.main} !important`, + }, + default: { + background: 'none', + color: colorVars.semantic.text.grey, + boxShadow: `inset 0 0 0 1px ${colorVars.semantic.ui.border}`, + }, + }, + variant: { + primary: { + selectors: { + '&:hover': { + background: colorVars.semantic.transparent.primaryHover, + color: colorVars.semantic.text.grey, + boxShadow: `inset 0 0 0 1px ${colorVars.semantic.ui.border}`, + }, + }, + }, + ghost: { + background: colorVars.primitive.blackAlpha[50], + color: colorVars.semantic.text.black, + selectors: { + '&:hover': { + background: colorVars.primitive.blackAlpha[200], + }, + }, + }, + }, + }, + defaultVariants: { + variant: 'primary', + }, +}) + +export interface Props extends JSX.ButtonHTMLAttributes { + state?: 'active' | 'default' + variant?: 'primary' | 'ghost' +} + +export const TabRound: ParentComponent = (props) => { + const [addedProps, originalButtonProps] = splitProps(props, ['state', 'variant', 'children']) + + return ( + + {addedProps.children} + + ) +} diff --git a/dashboard/src/components/UI/TextField.tsx b/dashboard/src/components/UI/TextField.tsx new file mode 100644 index 000000000..a1b4a53b3 --- /dev/null +++ b/dashboard/src/components/UI/TextField.tsx @@ -0,0 +1,213 @@ +import { TextField as KTextField } from '@kobalte/core' +import { style } from '@macaron-css/core' +import { styled } from '@macaron-css/solid' +import { Component, JSX, Show, splitProps } from 'solid-js' +import { writeToClipboard } from '/@/libs/clipboard' +import { colorVars, textVars } from '/@/theme' +import { RequiredMark, TitleContainer, containerStyle, errorTextStyle, titleStyle } from '../templates/FormItem' +import { MaterialSymbols } from './MaterialSymbols' +import { ToolTip, TooltipProps } from './ToolTip' +import { TooltipInfoIcon } from './TooltipInfoIcon' + +const Container = styled('div', { + base: { + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: '4px', + }, +}) +export const ActionsContainer = styled('div', { + base: { + position: 'relative', + width: '100%', + display: 'flex', + gap: '1px', + }, +}) +const inputStyle = style({ + width: '100%', + height: '100%', + padding: '0', + border: 'none', + + selectors: { + '&::placeholder': { + color: colorVars.semantic.text.disabled, + }, + '&:focus-visible': { + outline: 'none', + }, + }, +}) +const InputContainer = styled('div', { + base: { + width: '100%', + height: '48px', + padding: '0 16px', + display: 'flex', + gap: '4px', + + background: colorVars.semantic.ui.primary, + borderRadius: '8px', + border: 'none', + outline: `1px solid ${colorVars.semantic.ui.border}`, + color: colorVars.semantic.text.black, + ...textVars.text.regular, + + selectors: { + '&:focus-within': { + outline: `2px solid ${colorVars.semantic.primary.main}`, + }, + [`&:has(${inputStyle}[data-disabled])`]: { + cursor: 'not-allowed', + background: colorVars.semantic.ui.tertiary, + }, + [`&:has(${inputStyle}[data-invalid])`]: { + outline: `2px solid ${colorVars.semantic.accent.error}`, + }, + }, + }, + variants: { + copyable: { + true: { + borderRadius: '8px 0 0 8px', + }, + }, + }, +}) +const Icon = styled('div', { + base: { + color: colorVars.semantic.text.disabled, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, +}) +const CopyButton = styled('button', { + base: { + width: '48px', + flexShrink: 0, + borderRadius: '0 8px 8px 0', + border: 'none', + outline: `1px solid ${colorVars.semantic.ui.border}`, + + cursor: 'pointer', + color: colorVars.semantic.text.black, + background: colorVars.primitive.blackAlpha[100], + lineHeight: 1, + + selectors: { + '&:hover': { + background: colorVars.primitive.blackAlpha[200], + }, + '&:active': { + background: colorVars.primitive.blackAlpha[300], + }, + }, + }, +}) +const textareaStyle = style({ + width: '100%', + height: '100%', + padding: '10px 16px', + + background: colorVars.semantic.ui.primary, + borderRadius: '8px', + border: 'none', + outline: `1px solid ${colorVars.semantic.ui.border}`, + wordBreak: 'break-all', + color: colorVars.semantic.text.black, + ...textVars.text.regular, + resize: 'none', + + selectors: { + '&::placeholder': { + color: colorVars.semantic.text.disabled, + }, + '&:focus-visible': { + outline: `2px solid ${colorVars.semantic.primary.main}`, + }, + '&[data-disabled]': { + cursor: 'not-allowed', + background: colorVars.semantic.ui.tertiary, + }, + '&[data-invalid]': { + outline: `2px solid ${colorVars.semantic.accent.error}`, + }, + }, +}) + +export interface Props extends JSX.InputHTMLAttributes { + value?: string + error?: string + label?: string + leftIcon?: JSX.Element + rightIcon?: JSX.Element + multiline?: boolean + info?: TooltipProps + tooltip?: TooltipProps + copyable?: boolean + ref?: (element: HTMLInputElement | HTMLTextAreaElement) => void + onInput?: JSX.EventHandler + onChange?: JSX.EventHandler + onBlur?: JSX.EventHandler +} + +export const TextField: Component = (props) => { + const [rootProps, _addedProps, inputProps] = splitProps( + props, + ['name', 'value', 'required', 'disabled', 'readOnly'], + ['label', 'leftIcon', 'rightIcon', 'info', 'tooltip', 'copyable'], + ) + + const handleCopy = () => { + if (rootProps.value) { + writeToClipboard(rootProps.value.toString()) + } + } + + return ( + + + + {props.label} + + * + + + + + + + + + + + + {props.leftIcon} + + + + {props.rightIcon} + + + + + content_copy + + + + + + } + > + + + {props.error} + + ) +} diff --git a/dashboard/src/components/UI/ToolTip.tsx b/dashboard/src/components/UI/ToolTip.tsx new file mode 100644 index 000000000..dd431f4ee --- /dev/null +++ b/dashboard/src/components/UI/ToolTip.tsx @@ -0,0 +1,69 @@ +import { styled } from '@macaron-css/solid' +import { FlowComponent, JSX, children, mergeProps, onMount, splitProps } from 'solid-js' +import { TippyOptions, tippy } from 'solid-tippy' +import { Props } from 'tippy.js' +import 'tippy.js/animations/shift-away-subtle.css' +import 'tippy.js/dist/tippy.css' + +const TooltipContainer = styled('div', { + base: { + display: 'flex', + flexDirection: 'column', + }, + variants: { + align: { + left: { + alignItems: 'flex-start', + }, + center: { + alignItems: 'center', + }, + }, + }, +}) + +export type TooltipProps = Omit & { + props?: Partial< + Omit & { + content?: JSX.Element + } + > +} & { + /** + * @default "center" + */ + style?: 'left' | 'center' +} + +export const ToolTip: FlowComponent = (props) => { + const defaultOptions: TooltipProps = { + style: 'center', + hidden: true, + props: { + allowHTML: true, + maxWidth: 1000, + animation: 'shift-away-subtle', + }, + disabled: props.props?.content === undefined, + } + const propsWithDefaults = mergeProps(defaultOptions, props) + const [addedProps, tippyProps] = splitProps(propsWithDefaults, ['style', 'children']) + const c = children(() => props.children) + + onMount(() => { + for (const child of c.toArray()) { + if (child instanceof Element) { + tippy(child, () => ({ + ...tippyProps, + props: { + content: ( + {propsWithDefaults.props?.content} + ) as Element, + }, + })) + } + } + }) + + return <>{c()} +} diff --git a/dashboard/src/components/UI/TooltipInfoIcon.tsx b/dashboard/src/components/UI/TooltipInfoIcon.tsx new file mode 100644 index 000000000..ca3d94b7c --- /dev/null +++ b/dashboard/src/components/UI/TooltipInfoIcon.tsx @@ -0,0 +1,14 @@ +import { Component } from 'solid-js' +import { colorVars } from '/@/theme' +import { MaterialSymbols } from './MaterialSymbols' +import { ToolTip, TooltipProps } from './ToolTip' + +export const TooltipInfoIcon: Component = (props) => { + return ( + + + help + + + ) +} diff --git a/dashboard/src/components/URLText.tsx b/dashboard/src/components/UI/URLText.tsx similarity index 59% rename from dashboard/src/components/URLText.tsx rename to dashboard/src/components/UI/URLText.tsx index 7da0c10cb..6057ad136 100644 --- a/dashboard/src/components/URLText.tsx +++ b/dashboard/src/components/UI/URLText.tsx @@ -1,18 +1,15 @@ -import { vars } from '/@/theme' import { styled } from '@macaron-css/solid' -import { VsLinkExternal } from 'solid-icons/vs' import { Component } from 'solid-js' -import { tippy as tippyDir } from 'solid-tippy' - -// https://github.com/solidjs/solid/discussions/845 -const tippy = tippyDir +import { colorVars, textVars } from '/@/theme' +import { MaterialSymbols } from './MaterialSymbols' +import { ToolTip } from './ToolTip' const StyledAnchor = styled('a', { base: { - color: vars.text.url, + color: colorVars.semantic.text.link, + ...textVars.text.regular, }, }) - const ContentContainer = styled('div', { base: { display: 'flex', @@ -29,19 +26,18 @@ export interface URLTextProps { export const URLText: Component = (props) => { return ( -
    {props.text} - + open_in_new -
    + ) } diff --git a/dashboard/src/components/UserAvatar.tsx b/dashboard/src/components/UI/UserAvater.tsx similarity index 95% rename from dashboard/src/components/UserAvatar.tsx rename to dashboard/src/components/UI/UserAvater.tsx index fe7b5dc63..467909b40 100644 --- a/dashboard/src/components/UserAvatar.tsx +++ b/dashboard/src/components/UI/UserAvater.tsx @@ -1,6 +1,6 @@ -import { User } from '/@/api/neoshowcase/protobuf/gateway_pb' import { styled } from '@macaron-css/solid' import { Component, JSX, splitProps } from 'solid-js' +import { User } from '/@/api/neoshowcase/protobuf/gateway_pb' const UserAvatarImg = styled('img', { base: { @@ -23,6 +23,7 @@ const UserAvatar: Component = (props) => { style={{ width: addedProps.size ? `${addedProps.size}px` : '100%', }} + alt={addedProps.user.name} {...originalImgProps} /> ) diff --git a/dashboard/src/components/UI/UserMenuButton.tsx b/dashboard/src/components/UI/UserMenuButton.tsx new file mode 100644 index 000000000..4bd3cb594 --- /dev/null +++ b/dashboard/src/components/UI/UserMenuButton.tsx @@ -0,0 +1,139 @@ +import { DropdownMenu } from '@kobalte/core' +import { keyframes, style } from '@macaron-css/core' +import { styled } from '@macaron-css/solid' +import { A } from '@solidjs/router' +import { Component } from 'solid-js' +import { User } from '/@/api/neoshowcase/protobuf/gateway_pb' +import { colorVars, media, textVars } from '/@/theme' +import { Button } from './Button' +import { MaterialSymbols } from './MaterialSymbols' +import UserAvatar from './UserAvater' + +const triggerStyle = style({ + position: 'relative', + width: 'fit-content', + height: '44px', + padding: '0 8px', + display: 'flex', + alignItems: 'center', + gap: '8px', + cursor: 'pointer', + + border: 'none', + borderRadius: '8px', + background: 'none', + + selectors: { + '&:hover': { + background: colorVars.semantic.transparent.primaryHover, + }, + '&:active': { + background: colorVars.semantic.transparent.primarySelected, + }, + }, +}) +const UserName = styled('span', { + base: { + color: colorVars.semantic.text.black, + ...textVars.text.bold, + + '@media': { + [media.mobile]: { + display: 'none', + }, + }, + }, +}) +const iconStyle = style({ + width: '24px', + height: '24px', + transition: 'transform 0.2s', + selectors: { + '&[data-expanded]': { + transform: 'rotate(180deg)', + }, + }, +}) +const contentShowKeyframes = keyframes({ + from: { opacity: 0, transform: 'translateY(-8px)' }, + to: { opacity: 1, transform: 'translateY(0)' }, +}) +const contentHideKeyframes = keyframes({ + from: { opacity: 1, transform: 'translateY(0)' }, + to: { opacity: 0, transform: 'translateY(-8px)' }, +}) +const contentStyle = style({ + padding: '6px', + display: 'flex', + flexDirection: 'column', + + background: colorVars.semantic.ui.primary, + borderRadius: '6px', + boxShadow: '0px 0px 20px 0px rgba(0, 0, 0, 0.10)', + zIndex: 1, + + transformOrigin: 'var(--kb-menu-content-transform-origin)', + animation: `${contentHideKeyframes} 0.2s ease-in-out`, + selectors: { + '&[data-expanded]': { + animation: `${contentShowKeyframes} 0.2s ease-in-out`, + }, + }, +}) + +export const UserMenuButton: Component<{ + user: User +}> = (props) => { + return ( + + + + {props.user.name} + + arrow_drop_down + + + + + + + + + + + + + + + + + + ) + // setShowOptions((s) => !s)}> + // + // {props.user.name} + // arrow_drop_down + // + //
    setShowOptions(true)} + // use:clickOutside={() => setShowOptions(false)} + // class={optionsContainerClass} + // > + // + // + // + // + // + // + //
    + //
    + //
    +} diff --git a/dashboard/src/components/UserSearch.tsx b/dashboard/src/components/UserSearch.tsx deleted file mode 100644 index 5c8029f85..000000000 --- a/dashboard/src/components/UserSearch.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { User } from '/@/api/neoshowcase/protobuf/gateway_pb' -import { InputBar } from '/@/components/Input' -import UserAvatar from '/@/components/UserAvatar' -import { vars } from '/@/theme' -import { styled } from '@macaron-css/solid' -import Fuse from 'fuse.js' -import { FlowComponent, For, JSX, Show, createMemo, createSignal } from 'solid-js' - -const UserSearchContainer = styled('div', { - base: { - display: 'flex', - flexDirection: 'column', - gap: '16px', - }, -}) -const UserRow = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - gap: '16px', - }, -}) -const UserContainer = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - gap: '8px', - alignItems: 'center', - }, -}) -const UserName = styled('div', { - base: { - fontSize: '16px', - color: vars.text.black1, - }, -}) -const UsersList = styled('div', { - base: { - display: 'flex', - flexDirection: 'column', - gap: '8px', - overflowY: 'auto', - maxHeight: '400px', - }, -}) -const NoUsersFound = styled('div', { - base: { - color: vars.text.black2, - textAlign: 'center', - }, -}) - -export const UserSearch: FlowComponent< - { - users: User[] - }, - (user: User) => JSX.Element -> = (props) => { - const [userSearchQuery, setUserSearchQuery] = createSignal('') - - // users()の更新時にFuseインスタンスを再生成する - const fuse = createMemo( - () => - new Fuse(props.users, { - keys: ['name'], - }), - ) - // userSearchQuery()の更新時に検索を実行する - const userSearchResults = createMemo(() => { - // 検索クエリが空の場合は全ユーザーを表示する - if (userSearchQuery() === '') { - return props.users - } else { - return fuse() - .search(userSearchQuery()) - .map((result) => result.item) - } - }) - - return ( - - setUserSearchQuery(e.target.value)} - placeholder="search users..." - /> - - - {(user) => ( - - - - {user.name} - - {props.children(user)} - - )} - - - No Users Found - - - - ) -} diff --git a/dashboard/src/components/WebsiteSettings.tsx b/dashboard/src/components/WebsiteSettings.tsx deleted file mode 100644 index 4a2a5843b..000000000 --- a/dashboard/src/components/WebsiteSettings.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { AuthenticationType, CreateWebsiteRequest } from '/@/api/neoshowcase/protobuf/gateway_pb' -import { FormButton, FormCheckBox, FormSettings, FormSettingsButton, SettingsContainer } from '/@/components/AppsNew' -import { Button } from '/@/components/Button' -import { Checkbox } from '/@/components/Checkbox' -import { InfoTooltip } from '/@/components/InfoTooltip' -import { InputBar, InputLabel } from '/@/components/Input' -import { Select, SelectItem } from '/@/components/Select' -import { PlainMessage } from '@bufbuild/protobuf' -import { styled } from '@macaron-css/solid' -import { AiOutlinePlusCircle } from 'solid-icons/ai' -import { FaRegularTrashCan } from 'solid-icons/fa' -import { For, Show } from 'solid-js' -import { SetStoreFunction } from 'solid-js/store' -import { systemInfo } from '../libs/api' -import { vars } from '../theme' - -const AvailableDomainContainer = styled('div', { - base: { - fontSize: '14px', - color: vars.text.black2, - padding: '8px', - }, -}) - -const AvailableDomainUl = styled('ul', { - base: { - margin: '8px 0', - }, -}) - -const URLContainer = styled('div', { - base: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - gap: '2px', - }, -}) - -const URLWarning = styled('div', { - base: { - color: vars.icon.error, - fontWeight: 700, - marginTop: '4px', - }, -}) - -interface WebsiteSettingProps { - runtime: boolean - website: PlainMessage - setWebsite: >( - valueName: T, - value: PlainMessage[T], - ) => void - deleteWebsite: () => void -} - -const schemeOptions: SelectItem<'http' | 'https'>[] = [ - { value: 'http', title: 'http' }, - { value: 'https', title: 'https' }, -] - -const authenticationTypeItems: SelectItem[] = [ - { value: AuthenticationType.OFF, title: 'OFF' }, - { value: AuthenticationType.SOFT, title: 'SOFT' }, - { value: AuthenticationType.HARD, title: 'HARD' }, -] - -export const WebsiteSetting = (props: WebsiteSettingProps) => { - return ( - -
    - URL - - props.setWebsite('authentication', selected)} - /> -
    -
    - Advanced - - props.setWebsite('stripPrefix', selected)} - > - Strip Path Prefix - - - - props.setWebsite('h2c', selected)}> - h2c - - - - -
    - - - -
    - ) -} - -const newWebsite = (): PlainMessage => ({ - fqdn: '', - pathPrefix: '/', - stripPrefix: false, - https: true, - h2c: false, - httpPort: 0, - authentication: AuthenticationType.OFF, -}) - -interface WebsiteSettingsProps { - runtime: boolean - websiteConfigs: PlainMessage[] - setWebsiteConfigs: SetStoreFunction[]> -} - -export const WebsiteSettings = (props: WebsiteSettingsProps) => { - return ( - - - 使用可能なホスト - - !ad.alreadyBound) || []}> - {(domain) => ( -
  • - {domain.domain} - 0}> ({domain.excludeDomains.join(', ')}を除く) - :{domain.authAvailable ? '部員認証の使用可能' : '部員認証の使用不可'} -
  • - )} -
    -
    -
    - - {(website, i) => ( - { - props.setWebsiteConfigs(i(), valueName, value) - }} - deleteWebsite={() => - props.setWebsiteConfigs((current) => [...current.slice(0, i()), ...current.slice(i() + 1)]) - } - /> - )} - - - - - -
    - ) -} diff --git a/dashboard/src/components/layouts/DataTable.tsx b/dashboard/src/components/layouts/DataTable.tsx new file mode 100644 index 000000000..273bcfda7 --- /dev/null +++ b/dashboard/src/components/layouts/DataTable.tsx @@ -0,0 +1,42 @@ +import { styled } from '@macaron-css/solid' +import { colorVars, textVars } from '/@/theme' + +const Container = styled('div', { + base: { + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: '16px', + }, +}) +const Title = styled('h2', { + base: { + width: '100%', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + color: colorVars.semantic.text.black, + ...textVars.h2.medium, + }, +}) +const SubTitle = styled('div', { + base: { + color: colorVars.semantic.text.grey, + ...textVars.caption.medium, + }, +}) +const Titles = styled('div', { + base: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + }, +}) + +export const DataTable = { + Container, + Titles, + Title, + SubTitle, +} diff --git a/dashboard/src/components/layouts/ErrorView.tsx b/dashboard/src/components/layouts/ErrorView.tsx new file mode 100644 index 000000000..8c2c21797 --- /dev/null +++ b/dashboard/src/components/layouts/ErrorView.tsx @@ -0,0 +1,74 @@ +import { styled } from '@macaron-css/solid' +import { A } from '@solidjs/router' +import { Component, Show } from 'solid-js' +import { colorVars, textVars } from '/@/theme' +import { Button } from '../UI/Button' +import { MaterialSymbols } from '../UI/MaterialSymbols' + +const Container = styled('div', { + base: { + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: '1rem', + }, +}) +const Title = styled('h2', { + base: { + color: colorVars.semantic.accent.error, + ...textVars.h2.bold, + }, +}) +const Message = styled('p', { + base: { + color: colorVars.semantic.text.grey, + ...textVars.caption.medium, + }, +}) +const ButtonsContainer = styled('div', { + base: { + display: 'flex', + flexDirection: 'row', + gap: '8px', + }, +}) + +const ErrorView: Component<{ + error: unknown +}> = (props) => { + const handleReload = () => { + window.location.reload() + } + + return ( + + + error + + An error has occurred + + {(props.error as Error).message} + + + + + + + + + ) +} + +export default ErrorView diff --git a/dashboard/src/components/layouts/FormBox.tsx b/dashboard/src/components/layouts/FormBox.tsx new file mode 100644 index 000000000..bfc8bec63 --- /dev/null +++ b/dashboard/src/components/layouts/FormBox.tsx @@ -0,0 +1,42 @@ +import { styled } from '@macaron-css/solid' +import { colorVars } from '/@/theme' + +const Container = styled('div', { + base: { + width: '100%', + borderRadius: '8px', + border: `1px solid ${colorVars.semantic.ui.border}`, + background: colorVars.semantic.ui.primary, + }, +}) +const Forms = styled('div', { + base: { + width: '100%', + padding: '20px 24px', + display: 'flex', + flexDirection: 'column', + gap: '24px', + }, +}) +const Actions = styled('div', { + base: { + width: '100%', + padding: '16px 24px', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-end', + gap: '8px', + background: colorVars.semantic.ui.secondary, + borderTop: `1px solid ${colorVars.semantic.ui.border}`, + borderRadius: '0 0 8px 8px', + }, +}) + +const FormBox = { + Container, + Forms, + Actions, +} + +export default FormBox diff --git a/dashboard/src/components/layouts/MainView.tsx b/dashboard/src/components/layouts/MainView.tsx new file mode 100644 index 000000000..ac9849dec --- /dev/null +++ b/dashboard/src/components/layouts/MainView.tsx @@ -0,0 +1,41 @@ +import { styled } from '@macaron-css/solid' +import { colorVars, media } from '/@/theme' + +export const MainViewContainer = styled('div', { + base: { + position: 'relative', + width: '100%', + height: '100%', + padding: '40px max(calc(50% - 500px), 32px) 72px', + + '@media': { + [media.mobile]: { + padding: '40px 16px 72px', + }, + }, + }, + variants: { + background: { + grey: { + background: colorVars.semantic.ui.background, + }, + white: { + background: colorVars.semantic.ui.primary, + }, + }, + scrollable: { + true: { + overflowY: 'auto', + scrollbarGutter: 'stable', + }, + false: { + overflowY: 'hidden', + scrollbarGutter: 'none', + }, + }, + }, + defaultVariants: { + background: 'white', + scrollable: true, + }, +}) diff --git a/dashboard/src/components/layouts/SideView.tsx b/dashboard/src/components/layouts/SideView.tsx new file mode 100644 index 000000000..b0d2b5e70 --- /dev/null +++ b/dashboard/src/components/layouts/SideView.tsx @@ -0,0 +1,36 @@ +import { styled } from '@macaron-css/solid' + +const Container = styled('div', { + base: { + width: '100%', + display: 'grid', + gridTemplateColumns: '235px minmax(0, 1fr)', + gap: '48px', + + '@media': { + 'screen and (max-width: 1024px)': { + gridTemplateColumns: '1fr', + gridTemplateRows: 'auto auto', + gap: '24px', + }, + }, + }, +}) +const Side = styled('div', { + base: { + width: '100%', + height: '100%', + }, +}) +const Main = styled('div', { + base: { + width: '100%', + height: '100%', + }, +}) + +export const SideView = { + Container, + Side, + Main, +} diff --git a/dashboard/src/components/layouts/SuspenseContainer.tsx b/dashboard/src/components/layouts/SuspenseContainer.tsx new file mode 100644 index 000000000..6f909690a --- /dev/null +++ b/dashboard/src/components/layouts/SuspenseContainer.tsx @@ -0,0 +1,21 @@ +import { styled } from '@macaron-css/solid' + +const SuspenseContainer = styled('div', { + base: { + width: '100%', + height: '100%', + + opacity: 1, + transition: 'opacity 0.2s ease-in-out', + }, + variants: { + isPending: { + true: { + opacity: 0.5, + pointerEvents: 'none', + }, + }, + }, +}) + +export default SuspenseContainer diff --git a/dashboard/src/components/layouts/WithHeader.tsx b/dashboard/src/components/layouts/WithHeader.tsx new file mode 100644 index 000000000..9bbc5e51a --- /dev/null +++ b/dashboard/src/components/layouts/WithHeader.tsx @@ -0,0 +1,29 @@ +import { styled } from '@macaron-css/solid' +import { ParentComponent } from 'solid-js' +import { Header } from '../templates/Header' + +const Container = styled('div', { + base: { + width: '100%', + height: '100%', + display: 'grid', + gridTemplateColumns: '1fr', + gridTemplateRows: 'auto 1fr', + }, +}) +const Body = styled('div', { + base: { + width: '100%', + height: '100%', + overflowY: 'hidden', + }, +}) + +export const WithHeader: ParentComponent = (props) => { + return ( + +
    + {props.children} + + ) +} diff --git a/dashboard/src/components/layouts/WithNav.tsx b/dashboard/src/components/layouts/WithNav.tsx new file mode 100644 index 000000000..27fdd38bd --- /dev/null +++ b/dashboard/src/components/layouts/WithNav.tsx @@ -0,0 +1,52 @@ +import { styled } from '@macaron-css/solid' +import { colorVars, media } from '/@/theme' + +const Container = styled('div', { + base: { + width: '100%', + height: '100%', + display: 'grid', + gridTemplateColumns: '1fr', + gridTemplateRows: 'auto 1fr', + }, +}) +const Navs = styled('div', { + base: { + width: '100%', + height: 'auto', + overflowX: 'hidden', + borderBottom: `1px solid ${colorVars.semantic.ui.border}`, + }, +}) +const Body = styled('div', { + base: { + position: 'relative', + width: '100%', + height: '100%', + overflowY: 'hidden', + }, +}) +const TabContainer = styled('div', { + base: { + width: '100%', + maxWidth: 'min(1000px, calc(100% - 64px))', + margin: '0 auto', + display: 'flex', + gap: '8px', + padding: '0 0 16px 0', + overflowX: 'auto', + + '@media': { + [media.mobile]: { + maxWidth: 'min(1000px, calc(100% - 32px))', + }, + }, + }, +}) + +export const WithNav = { + Container, + Navs, + Tabs: TabContainer, + Body, +} diff --git a/dashboard/src/components/templates/AppsNav.tsx b/dashboard/src/components/templates/AppsNav.tsx new file mode 100644 index 000000000..6f18e4eb8 --- /dev/null +++ b/dashboard/src/components/templates/AppsNav.tsx @@ -0,0 +1,6 @@ +import { Component } from 'solid-js' +import { Nav } from './Nav' + +export const AppsNav: Component = () => { + return