diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index d24b483..f1b2cbd 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -19,6 +19,7 @@ jobs: npm ci npm run build cd dist && zip -qr ../dist.zip . && cd .. + sha256sum dist.zip > dist.zip.sha256 - name: 提取 CHANGELOG 版本段作为 Release 描述 run: | @@ -31,4 +32,6 @@ jobs: with: token: ${{ secrets.BUILD_TOKEN }} body_path: release-notes.md - files: dist.zip + files: | + dist.zip + dist.zip.sha256 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63de34f..65e4a14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,7 @@ jobs: npm ci npm run build cd dist && zip -qr ../dist.zip . && cd .. + sha256sum dist.zip > dist.zip.sha256 ls -lh dist.zip - name: 提取 CHANGELOG 版本段作为 Release 描述 @@ -37,4 +38,6 @@ jobs: with: token: ${{ github.token }} body_path: release-notes.md - files: dist.zip + files: | + dist.zip + dist.zip.sha256 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a986a9..791a7de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ 格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。 +## [0.1.0] - 2026-07-10 + +### Added + +- 设置页「通知渠道」卡片:Webhook(Slack / 飞书 / 钉钉 / 企微预设)、ntfy、Bark、SMTP 与 Telegram 五渠道管理 +- 日志页审计告警规则管理:列表、组合匹配表单与通知模板入口 +- 账号安全新增「撤销全部会话」入口 +- 租户暂停状态三态展示(详情 / 列表 / 总览) + +### Changed + +- 实例详情 root 密码默认掩码、点击查看;串口控制台移除「复制 root 密码」 +- 修改登录凭据后当前会话无感续期;其余敏感操作同样自动换用新令牌 +- 域选择器仅显示域名(去除许可类型后缀) + +### Fixed + +- 明暗主题扩散切换时的闪烁 +- 任务表单数字输入框在 small 尺寸下步进按钮溢出 +- 租户详情「用户」「策略」等 Tab 切换域后内容不刷新 +- 实例详情区块加载中时禁用头部操作按钮,防误点 + ## [0.0.1] - 2026-07-09 首个版本:OCI Portal 前端。 diff --git a/src/api/auth.ts b/src/api/auth.ts index 4289b8e..4bdae8e 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -5,6 +5,7 @@ import type { LoginRequest, LoginResponse, OAuthSettings, + SessionRefresh, TotpSetup, TotpStatus, UpdateCredentialsRequest, @@ -26,6 +27,14 @@ export function logout(): Promise { return request('/auth/logout', { method: 'POST' }) } +// ---- 会话 ---- + +/** 撤销全部会话:旧 token 全部失效,响应带操作者的新会话 */ +export function revokeSessions(): Promise { + if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300) + return request('/auth/revoke-sessions', { method: 'POST' }) +} + // ---- 登录凭据 ---- export function getCredentials(): Promise { @@ -33,15 +42,15 @@ export function getCredentials(): Promise { return request('/auth/credentials') } -/** 修改用户名 / 密码;成功后旧 token 随即失效,调用方应登出重登 */ -export function updateCredentials(body: UpdateCredentialsRequest): Promise { - if (mockOn) return mocked(undefined, 400) +/** 修改用户名 / 密码;成功后旧 token 全部失效,响应带操作者的新会话 */ +export function updateCredentials(body: UpdateCredentialsRequest): Promise { + if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 400) return request('/auth/credentials', { method: 'PUT', body }) } -/** 保存密码登录禁用开关;开启要求至少绑定一个外部身份(后端 409) */ -export function updatePasswordLogin(disabled: boolean): Promise { - if (mockOn) return mocked(undefined, 300) +/** 保存密码登录禁用开关;成功后旧 token 全部失效,响应带新会话 */ +export function updatePasswordLogin(disabled: boolean): Promise { + if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request('/auth/password-login', { method: 'PUT', body: { disabled } }) } @@ -62,14 +71,15 @@ export function setupTotp(): Promise { return request('/auth/totp/setup', { method: 'POST' }) } -export function activateTotp(code: string): Promise { - if (mockOn) return mocked(undefined, 300) +/** 激活两步验证;成功后旧 token 全部失效,响应带新会话 */ +export function activateTotp(code: string): Promise { + if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request('/auth/totp/activate', { method: 'POST', body: { code } }) } -/** 停用两步验证;密码或当前验证码任一确认 */ -export function disableTotp(body: { password?: string; code?: string }): Promise { - if (mockOn) return mocked(undefined, 300) +/** 停用两步验证;密码或当前验证码任一确认,响应带新会话 */ +export function disableTotp(body: { password?: string; code?: string }): Promise { + if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request('/auth/totp/disable', { method: 'POST', body }) } @@ -92,7 +102,7 @@ export function getOauthProviders(): Promise<{ return request('/auth/oauth/providers') } -/** 获取授权跳转 URL;bind 模式要求已登录(带 JWT) */ +/** 获取授权跳转 URL;bind 模式要求已登录 */ export function getOauthAuthorizeUrl( provider: string, mode: 'login' | 'bind', @@ -111,8 +121,9 @@ export function listIdentities(): Promise<{ items: UserIdentityInfo[] }> { return request('/auth/identities') } -export function unbindIdentity(id: number): Promise { - if (mockOn) return mocked(undefined, 300) +/** 解绑外部身份;成功后旧 token 全部失效,响应带新会话 */ +export function unbindIdentity(id: number): Promise { + if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request(`/auth/identities/${id}`, { method: 'DELETE' }) } diff --git a/src/api/logevents.ts b/src/api/logevents.ts index e07d61a..67e8b33 100644 --- a/src/api/logevents.ts +++ b/src/api/logevents.ts @@ -1,6 +1,13 @@ import { mockLogEvents, mockLogRelays, mockLogWebhooks } from './mock' import { mockOn, mocked, request } from './request' -import type { LogEventPage, LogRelayView, LogWebhookInfo, LogWebhookState } from '@/types/api' +import type { + AlertRule, + AlertRuleBody, + LogEventPage, + LogRelayView, + LogWebhookInfo, + LogWebhookState, +} from '@/types/api' export interface LogEventParams { page: number @@ -23,6 +30,27 @@ export function listLogEvents(params: LogEventParams): Promise { return request('/log-events', { query: { ...params } }) } +// ---- 告警规则 ---- +export function listAlertRules(): Promise { + if (mockOn) return mocked([]) + return request<{ items: AlertRule[] }>('/log-events/alert-rules').then((r) => r.items) +} + +export function createAlertRule(body: AlertRuleBody): Promise { + if (mockOn) return mocked({ ...body, id: 1, createdAt: '', updatedAt: '' }, 300) + return request('/log-events/alert-rules', { method: 'POST', body }) +} + +export function updateAlertRule(id: number, body: AlertRuleBody): Promise { + if (mockOn) return mocked({ ...body, id, createdAt: '', updatedAt: '' }, 300) + return request(`/log-events/alert-rules/${id}`, { method: 'PUT', body }) +} + +export function deleteAlertRule(id: number): Promise { + if (mockOn) return mocked(undefined, 300) + return request(`/log-events/alert-rules/${id}`, { method: 'DELETE' }) +} + export function getLogWebhook(cfgId: number): Promise { if (mockOn) { const info = mockLogWebhooks.get(cfgId) diff --git a/src/api/request.ts b/src/api/request.ts index d4b7706..2aa26a9 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -15,6 +15,8 @@ interface RequestOptions { method?: string body?: unknown query?: Record + /** 附加请求头 */ + headers?: Record } function buildUrl(path: string, query?: RequestOptions['query']): string { @@ -46,7 +48,7 @@ async function parseError(resp: Response): Promise { /** 统一请求封装:注入 JWT、401 登出、错误转 ApiError */ export async function request(path: string, opts: RequestOptions = {}): Promise { const auth = useAuthStore() - const headers: Record = { 'Content-Type': 'application/json' } + const headers: Record = { 'Content-Type': 'application/json', ...opts.headers } if (auth.token) headers.Authorization = `Bearer ${auth.token}` const resp = await fetch(buildUrl(path, opts.query), { method: opts.method ?? 'GET', diff --git a/src/api/settings.ts b/src/api/settings.ts index 4ba8f26..c574d7a 100644 --- a/src/api/settings.ts +++ b/src/api/settings.ts @@ -8,12 +8,15 @@ import { import { mockOn, mocked, request } from './request' import type { AboutInfo, + NotifyChannelItem, + NotifyChannelType, NotifyEventsSetting, NotifyTemplateItem, SecuritySetting, SystemLogPage, TaskSetting, TelegramSetting, + UpdateNotifyChannelBody, } from '@/types/api' export function getTelegramSetting(): Promise { @@ -46,6 +49,37 @@ export function testTelegram(): Promise { return request('/settings/telegram/test', { method: 'POST' }) } +/** 通知渠道(webhook/ntfy/bark/smtp)脱敏视图列表 */ +export function listNotifyChannels(): Promise { + if (mockOn) + return mocked( + (['webhook', 'ntfy', 'bark', 'smtp'] as NotifyChannelType[]).map((type) => ({ + type, + enabled: false, + secretSet: false, + })), + ) + return request<{ items: NotifyChannelItem[] }>('/settings/notify-channels').then((r) => r.items) +} + +/** 保存单渠道配置,返回全渠道最新视图 */ +export function updateNotifyChannel( + type: NotifyChannelType, + body: UpdateNotifyChannelBody, +): Promise { + if (mockOn) return mocked([], 400) + return request<{ items: NotifyChannelItem[] }>(`/settings/notify-channels/${type}`, { + method: 'PUT', + body, + }).then((r) => r.items) +} + +/** 向指定渠道发送测试消息(用已保存配置,不看启用开关) */ +export function testNotifyChannel(type: NotifyChannelType): Promise { + if (mockOn) return mocked(undefined, 800) + return request(`/settings/notify-channels/${type}/test`, { method: 'POST' }) +} + export function getNotifyEvents(): Promise { if (mockOn) return mocked({ ...mockNotifyEvents }) return request('/settings/notify-events') diff --git a/src/api/tenant.ts b/src/api/tenant.ts index 923dc70..2ed45a3 100644 --- a/src/api/tenant.ts +++ b/src/api/tenant.ts @@ -14,6 +14,7 @@ import { mockOn, mocked, request } from './request' import type { AuditEventsResult, IamUser, + IdentityDomain, IamUserDetail, IdentityProvider, IdentitySetting, @@ -83,14 +84,25 @@ export function getSubscription(id: number, subscriptionId: string): Promise { + if (mockOn) + return mocked([ + { id: 'ocid1.domain.default', displayName: 'Default', homeRegion: 'us-phoenix-1', type: 'DEFAULT', licenseType: 'free' }, + { id: 'ocid1.domain.idcs', displayName: 'OracleIdentityCloudService', homeRegion: 'us-phoenix-1', type: 'SECONDARY', licenseType: 'idcs-foundation' }, + ]) + return request(`/oci-configs/${id}/domains`) +} + // ---- 用户 ---- -export function listUsers(id: number): Promise { +export function listUsers(id: number, domainId?: string): Promise { if (mockOn) return mocked(mockUsers) - return request(`/oci-configs/${id}/users`) + return request(`/oci-configs/${id}/users`, { query: { domainId } }) } // 编辑表单预填充用;不在身份域的经典用户返回 inDomain: false -export function getUserDetail(id: number, userId: string): Promise { +export function getUserDetail(id: number, userId: string, domainId?: string): Promise { if (mockOn) return mocked( mockUserDetails[userId] ?? { @@ -101,7 +113,7 @@ export function getUserDetail(id: number, userId: string): Promise { +export function createUser(id: number, body: CreateUserBody, domainId?: string): Promise { if (mockOn) return mocked({ ...mockUsers[2], id: 'ocid1.user..new', name: body.name, email: body.email ?? '' }) - return request(`/oci-configs/${id}/users`, { method: 'POST', body }) + return request(`/oci-configs/${id}/users`, { method: 'POST', body, query: { domainId } }) } export interface UpdateUserBody { @@ -131,29 +143,29 @@ export interface UpdateUserBody { addToAdminGroup?: boolean } -export function updateUser(id: number, userId: string, body: UpdateUserBody): Promise { +export function updateUser(id: number, userId: string, body: UpdateUserBody, domainId?: string): Promise { if (mockOn) { const u = mockUsers.find((x) => x.id === userId) ?? mockUsers[2] if (body.description !== undefined) u.description = body.description if (body.email !== undefined) u.email = body.email return mocked({ ...u }) } - return request(`/oci-configs/${id}/users/${userId}`, { method: 'PUT', body }) + return request(`/oci-configs/${id}/users/${userId}`, { method: 'PUT', body, query: { domainId } }) } -export function deleteUser(id: number, userId: string): Promise { +export function deleteUser(id: number, userId: string, domainId?: string): Promise { if (mockOn) return mocked(undefined) - return request(`/oci-configs/${id}/users/${userId}`, { method: 'DELETE' }) + return request(`/oci-configs/${id}/users/${userId}`, { method: 'DELETE', query: { domainId } }) } -export function resetUserPassword(id: number, userId: string): Promise<{ password: string }> { +export function resetUserPassword(id: number, userId: string, domainId?: string): Promise<{ password: string }> { if (mockOn) return mocked({ password: 'Xy9#kQ2m$Lp8' }) - return request(`/oci-configs/${id}/users/${userId}/reset-password`, { method: 'POST' }) + return request(`/oci-configs/${id}/users/${userId}/reset-password`, { method: 'POST', query: { domainId } }) } -export function clearUserMfa(id: number, userId: string): Promise<{ deletedTotpDevices: number }> { +export function clearUserMfa(id: number, userId: string, domainId?: string): Promise<{ deletedTotpDevices: number }> { if (mockOn) return mocked({ deletedTotpDevices: 1 }) - return request(`/oci-configs/${id}/users/${userId}/mfa-devices`, { method: 'DELETE' }) + return request(`/oci-configs/${id}/users/${userId}/mfa-devices`, { method: 'DELETE', query: { domainId } }) } export function deleteUserApiKeys( @@ -169,26 +181,19 @@ export function deleteUserApiKeys( } // ---- 审计日志 ---- -/** 实时查询租户 OCI 审计事件:hours ∈ [1,72] 首查; - * 截断续查传上次响应的 start/end 与 nextPage(绝对窗+游标断点续翻) */ +/** 批式懒加载查询审计事件:cursor 空自当前时刻首查, + * 非空从上次响应游标向更早续取一批(~100 条,服务端分窗回溯) */ export function listAuditEvents( id: number, - opts: { hours?: number; region?: string; start?: string; end?: string; page?: string } = {}, + opts: { region?: string; cursor?: string; limit?: number } = {}, ): Promise { - // mock 下选 1 小时窗演示截断提示 + // mock:首批带游标,续查一批后到尽头,演示懒加载链路 if (mockOn) return mocked( - { - items: mockAuditEvents, - truncated: opts.hours === 1, - start: '2026-07-06T10:00:00Z', - end: '2026-07-07T10:00:00Z', - }, + { items: mockAuditEvents, cursor: opts.cursor ? undefined : 'mock-cursor', exhausted: !!opts.cursor }, 300, ) - const { hours = 24, region, start, end, page } = opts - const query = start && end ? { region, start, end, page } : { hours, region } - return request(`/oci-configs/${id}/audit-events`, { query }) + return request(`/oci-configs/${id}/audit-events`, { query: { ...opts } }) } /** 取回单条审计事件的原始 JSON;404 表示缓存过期且重查未找回(前端降级展示精简字段) */ @@ -201,45 +206,49 @@ export function getAuditEventDetail( } // ---- 域设置 ---- -export function getNotificationRecipients(id: number): Promise { +export function getNotificationRecipients(id: number, domainId?: string): Promise { if (mockOn) return mocked(mockRecipients) - return request(`/oci-configs/${id}/notification-recipients`) + return request(`/oci-configs/${id}/notification-recipients`, { query: { domainId } }) } export function updateNotificationRecipients( id: number, recipients: string[], + domainId?: string, ): Promise { if (mockOn) return mocked({ recipients, testModeEnabled: recipients.length > 0 }) return request(`/oci-configs/${id}/notification-recipients`, { method: 'PUT', body: { recipients }, + query: { domainId }, }) } -export function listPasswordPolicies(id: number): Promise { +export function listPasswordPolicies(id: number, domainId?: string): Promise { if (mockOn) return mocked(mockPolicies) - return request(`/oci-configs/${id}/password-policies`) + return request(`/oci-configs/${id}/password-policies`, { query: { domainId } }) } export function updatePasswordPolicy( id: number, policyId: string, body: { passwordExpiresAfter?: number; minLength?: number; numPasswordsInHistory?: number }, + domainId?: string, ): Promise { if (mockOn) return mocked({ ...mockPolicies[0], ...body }) - return request(`/oci-configs/${id}/password-policies/${policyId}`, { method: 'PUT', body }) + return request(`/oci-configs/${id}/password-policies/${policyId}`, { method: 'PUT', body, query: { domainId } }) } // ---- 身份设置 ---- -export function getIdentitySetting(id: number): Promise { +export function getIdentitySetting(id: number, domainId?: string): Promise { if (mockOn) return mocked({ ...mockIdentitySetting }) - return request(`/oci-configs/${id}/identity-settings`) + return request(`/oci-configs/${id}/identity-settings`, { query: { domainId } }) } export function updateIdentitySetting( id: number, primaryEmailRequired: boolean, + domainId?: string, ): Promise { if (mockOn) { mockIdentitySetting.primaryEmailRequired = primaryEmailRequired @@ -248,10 +257,11 @@ export function updateIdentitySetting( return request(`/oci-configs/${id}/identity-settings`, { method: 'PUT', body: { primaryEmailRequired }, + query: { domainId }, }) } -export async function downloadSamlMetadata(id: number): Promise { +export async function downloadSamlMetadata(id: number, domainId?: string): Promise { if (mockOn) { const blob = new Blob([''], { type: 'application/xml' }) triggerDownload(blob, 'oci-domain-saml-metadata.xml') @@ -259,7 +269,8 @@ export async function downloadSamlMetadata(id: number): Promise { } const { useAuthStore } = await import('@/stores/auth') const auth = useAuthStore() - const resp = await fetch(`/api/v1/oci-configs/${id}/saml-metadata`, { + const qs = domainId ? `?domainId=${encodeURIComponent(domainId)}` : '' + const resp = await fetch(`/api/v1/oci-configs/${id}/saml-metadata${qs}`, { headers: auth.token ? { Authorization: `Bearer ${auth.token}` } : {}, }) if (!resp.ok) throw new Error(`下载失败(${resp.status})`) @@ -293,7 +304,7 @@ export interface CreateIdpBody { jitMapEmail?: boolean } -export function createIdp(id: number, body: CreateIdpBody): Promise { +export function createIdp(id: number, body: CreateIdpBody, domainId?: string): Promise { if (mockOn) { const created = { ...mockIdps[1], @@ -304,41 +315,43 @@ export function createIdp(id: number, body: CreateIdpBody): Promise { +export function listIdps(id: number, domainId?: string): Promise { if (mockOn) return mocked(mockIdps) - return request(`/oci-configs/${id}/identity-providers`) + return request(`/oci-configs/${id}/identity-providers`, { query: { domainId } }) } -export function activateIdp(id: number, idpId: string, enabled: boolean): Promise { +export function activateIdp(id: number, idpId: string, enabled: boolean, domainId?: string): Promise { if (mockOn) return mocked({ ...mockIdps[0], enabled }) return request(`/oci-configs/${id}/identity-providers/${idpId}/activate`, { method: 'POST', body: { enabled }, + query: { domainId }, }) } -export function deleteIdp(id: number, idpId: string): Promise { +export function deleteIdp(id: number, idpId: string, domainId?: string): Promise { if (mockOn) return mocked(undefined) - return request(`/oci-configs/${id}/identity-providers/${idpId}`, { method: 'DELETE' }) + return request(`/oci-configs/${id}/identity-providers/${idpId}`, { method: 'DELETE', query: { domainId } }) } -export function listSignOnRules(id: number): Promise { +export function listSignOnRules(id: number, domainId?: string): Promise { if (mockOn) return mocked(mockSignOnRules) - return request(`/oci-configs/${id}/sign-on-rules`) + return request(`/oci-configs/${id}/sign-on-rules`, { query: { domainId } }) } -export function createSignOnExemption(id: number, identityProviderId: string): Promise { +export function createSignOnExemption(id: number, identityProviderId: string, domainId?: string): Promise { if (mockOn) return mocked(mockSignOnRules[0]) return request(`/oci-configs/${id}/sign-on-exemptions`, { method: 'POST', body: { identityProviderId }, + query: { domainId }, }) } -export function deleteSignOnExemption(id: number, ruleId: string): Promise { +export function deleteSignOnExemption(id: number, ruleId: string, domainId?: string): Promise { if (mockOn) return mocked(undefined) - return request(`/oci-configs/${id}/sign-on-exemptions/${ruleId}`, { method: 'DELETE' }) + return request(`/oci-configs/${id}/sign-on-exemptions/${ruleId}`, { method: 'DELETE', query: { domainId } }) } diff --git a/src/assets/main.css b/src/assets/main.css index fb90bcc..0168434 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -136,7 +136,9 @@ body { width: 26px; margin: 0; display: grid; - grid-template-rows: 1fr 1fr; + /* minmax(0,…) 允许行收缩:small 尺寸下按钮固有高度(18px)大于半行, + 1fr 的隐式 min-height:auto 会把行撑爆,下箭头溢出框外 */ + grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); border-left: 1px solid var(--color-line-soft); } .app-input-number .n-input__suffix > :last-child { @@ -158,6 +160,7 @@ body { --n-text-color-focus: var(--color-ink); width: 100%; height: 100%; + min-height: 0; border-radius: inherit; color: var(--color-ink-3); } @@ -258,3 +261,25 @@ body { } } } + +/* 明暗主题扩散切换(useThemeRipple):关掉 View Transitions 默认交叉淡入, + 由 JS 在 ::view-transition-new(root) 上做以触发点为圆心的 clip-path 揭示; + 旧快照垫底不动,blend 还原为普通覆盖,避免半透明混色。 */ +::view-transition-old(root), +::view-transition-new(root) { + animation: none; + mix-blend-mode: normal; +} +::view-transition-old(root) { + z-index: 1; +} +::view-transition-new(root) { + z-index: 2; +} +/* 扩散进行中禁掉页面自身的过渡:new 快照是实时层,Naive 等组件的 + 0.3s 颜色渐变会在扩散圆内继续播放,叠加揭示动画表现为闪烁/渗色。 */ +html.theme-switching *, +html.theme-switching *::before, +html.theme-switching *::after { + transition: none !important; +} diff --git a/src/components/AppLogo.vue b/src/components/AppLogo.vue index 87c56b9..6f5c7bc 100644 --- a/src/components/AppLogo.vue +++ b/src/components/AppLogo.vue @@ -1,30 +1,98 @@ + + diff --git a/src/components/DeadPlaceholder.vue b/src/components/DeadPlaceholder.vue index a7d6ebc..b39001a 100644 --- a/src/components/DeadPlaceholder.vue +++ b/src/components/DeadPlaceholder.vue @@ -1,7 +1,7 @@ diff --git a/src/components/FilePicker.vue b/src/components/FilePicker.vue index 1ce94f9..ec4becc 100644 --- a/src/components/FilePicker.vue +++ b/src/components/FilePicker.vue @@ -1,13 +1,13 @@ + + diff --git a/src/components/logs/LogEventPanel.vue b/src/components/logs/LogEventPanel.vue index d58876c..8e050bb 100644 --- a/src/components/logs/LogEventPanel.vue +++ b/src/components/logs/LogEventPanel.vue @@ -1,10 +1,11 @@ + + diff --git a/src/components/settings/NotifyTemplateModal.vue b/src/components/settings/NotifyTemplateModal.vue index e6c7ffb..90b5f43 100644 --- a/src/components/settings/NotifyTemplateModal.vue +++ b/src/components/settings/NotifyTemplateModal.vue @@ -1,9 +1,10 @@