feat: 接入真实平台登录态
- login/session: 对接后端登录、鉴权恢复和首次改密 - api/store: 恢复 BPMN XML 真实接口并携带 Bearer token - layout/settings: 展示真实角色、审计 JSON 和通知平台配置文档
This commit is contained in:
@@ -45,7 +45,10 @@ NOTIFICATION_WEBHOOK_URL=https://notification.example.internal/webhook/replace-w
|
||||
|
||||
## 企业微信配置
|
||||
|
||||
官方参考:`https://developer.work.weixin.qq.com/document/path/91770`
|
||||
参考文档:
|
||||
|
||||
- 企业微信开发者文档:`https://developer.work.weixin.qq.com/document/path/91770`
|
||||
- 腾讯云消息中心企业微信机器人接收消息:`https://cloud.tencent.com/document/product/1263/71731`
|
||||
|
||||
1. 在企业微信群中添加群机器人。
|
||||
2. 复制机器人 webhook,保存到服务端环境变量 `WECOM_WEBHOOK_URL`。
|
||||
|
||||
@@ -44,13 +44,44 @@ const previewText = computed(() => {
|
||||
return formattedJson.value.replace(/\s+/g, ' ')
|
||||
})
|
||||
|
||||
const canToggle = computed(
|
||||
() => formattedJson.value.length > 80 || formattedJson.value.includes('\n'),
|
||||
)
|
||||
const hasContent = computed(() => formattedJson.value !== '-')
|
||||
|
||||
async function copyJson() {
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(formattedJson.value)
|
||||
} else {
|
||||
fallbackCopy(formattedJson.value)
|
||||
}
|
||||
|
||||
ElMessage.success('已复制 JSON')
|
||||
} catch {
|
||||
try {
|
||||
fallbackCopy(formattedJson.value)
|
||||
ElMessage.success('已复制 JSON')
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请手动选择 JSON 内容')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text: string) {
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
textarea.setAttribute('readonly', 'readonly')
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.left = '-9999px'
|
||||
document.body.appendChild(textarea)
|
||||
|
||||
try {
|
||||
textarea.select()
|
||||
|
||||
if (!document.execCommand('copy')) {
|
||||
throw new Error('copy command rejected')
|
||||
}
|
||||
} finally {
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -61,7 +92,7 @@ async function copyJson() {
|
||||
:icon="expanded ? ArrowDown : ArrowRight"
|
||||
size="small"
|
||||
text
|
||||
:disabled="!canToggle"
|
||||
:disabled="!hasContent"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
{{ expanded ? '收起' : '展开' }}
|
||||
|
||||
@@ -50,9 +50,9 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
function logout() {
|
||||
session.logout()
|
||||
void router.push('/login')
|
||||
async function logout() {
|
||||
await session.logout()
|
||||
await router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -98,12 +98,12 @@ function logout() {
|
||||
</ElTag>
|
||||
<ElDropdown>
|
||||
<ElButton>
|
||||
{{ session.operator }}
|
||||
{{ session.displayName || session.operator }}
|
||||
<ElIcon class="el-icon--right"><Tools /></ElIcon>
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem disabled>角色:{{ session.role }}</ElDropdownItem>
|
||||
<ElDropdownItem disabled>角色:{{ session.roleLabel }}</ElDropdownItem>
|
||||
<ElDropdownItem :icon="SwitchButton" @click="logout">退出登录</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
|
||||
+4
-3
@@ -60,10 +60,11 @@ const router = createRouter({
|
||||
routes,
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
router.beforeEach(async (to) => {
|
||||
const session = useSessionStore()
|
||||
const authenticated = await session.bootstrap()
|
||||
|
||||
if (!to.meta.public && !session.isAuthenticated) {
|
||||
if (!to.meta.public && (!authenticated || session.mustChangePassword)) {
|
||||
return {
|
||||
name: 'login',
|
||||
query: {
|
||||
@@ -72,7 +73,7 @@ router.beforeEach((to) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (to.name === 'login' && session.isAuthenticated) {
|
||||
if (to.name === 'login' && authenticated && !session.mustChangePassword) {
|
||||
return {
|
||||
name: 'overview',
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
import type {
|
||||
AgentCard,
|
||||
AgentInvocation,
|
||||
AuthenticatedUser,
|
||||
AuditLogEntry,
|
||||
ChangePasswordPayload,
|
||||
CreateAgentInvocationPayload,
|
||||
CreateDeployRunPayload,
|
||||
DashboardSummary,
|
||||
@@ -16,6 +18,8 @@ import type {
|
||||
IntegrationVariableHelp,
|
||||
IntegrationStatus,
|
||||
JenkinsSyncSummary,
|
||||
LoginPayload,
|
||||
LoginResult,
|
||||
PipelineStep,
|
||||
ProjectConfig,
|
||||
ProjectRefs,
|
||||
@@ -216,6 +220,29 @@ export const devopsApi = {
|
||||
return lastDataSource
|
||||
},
|
||||
|
||||
async login(payload: LoginPayload): Promise<LoginResult> {
|
||||
const { data } = await http.post<LoginResult>('/auth/login', payload)
|
||||
lastDataSource = 'backend'
|
||||
return data
|
||||
},
|
||||
|
||||
async getCurrentUser(): Promise<AuthenticatedUser> {
|
||||
const { data } = await http.get<AuthenticatedUser>('/auth/me')
|
||||
lastDataSource = 'backend'
|
||||
return data
|
||||
},
|
||||
|
||||
async changePassword(payload: ChangePasswordPayload): Promise<AuthenticatedUser> {
|
||||
const { data } = await http.post<AuthenticatedUser>('/auth/change-password', payload)
|
||||
lastDataSource = 'backend'
|
||||
return data
|
||||
},
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await http.post('/auth/logout')
|
||||
lastDataSource = 'backend'
|
||||
},
|
||||
|
||||
async getDashboard(): Promise<DashboardSummary> {
|
||||
const [health, backendProjects, backendRuns, integrationConfig] = await Promise.all([
|
||||
fetchHealth(),
|
||||
|
||||
@@ -9,6 +9,18 @@ export const http = axios.create({
|
||||
withCredentials: true,
|
||||
})
|
||||
|
||||
/**
|
||||
* 登录令牌只用于调用后端 API,不会传给 Jenkins/Gitea/LLM 等第三方服务。
|
||||
*/
|
||||
export function setAuthToken(token?: string) {
|
||||
if (token) {
|
||||
http.defaults.headers.common.Authorization = `Bearer ${token}`
|
||||
return
|
||||
}
|
||||
|
||||
delete http.defaults.headers.common.Authorization
|
||||
}
|
||||
|
||||
http.interceptors.response.use(
|
||||
(response) => {
|
||||
response.data = unwrapApiPayload(response.data)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* 平台 Pinia store 聚合后端项目、发布、设置和审计数据。
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { devopsApi } from '../services/devopsApi'
|
||||
|
||||
+151
-8
@@ -1,28 +1,171 @@
|
||||
/**
|
||||
* 维护后端真实登录态,前端只保存 DevOps API token,不保存任何第三方密钥。
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { devopsApi } from '../services/devopsApi'
|
||||
import { setAuthToken } from '../services/http'
|
||||
import type {
|
||||
AuthenticatedUser,
|
||||
ChangePasswordPayload,
|
||||
LoginPayload,
|
||||
} from '../types/devops'
|
||||
|
||||
const STORAGE_KEY = 'devops-platform-session'
|
||||
|
||||
type StoredSession = {
|
||||
token: string
|
||||
expiresAt: string
|
||||
user: AuthenticatedUser
|
||||
}
|
||||
|
||||
export const useSessionStore = defineStore('session', () => {
|
||||
const operator = ref('ops-admin')
|
||||
const role = ref<'admin' | 'operator' | 'viewer'>('admin')
|
||||
const token = ref('local-session')
|
||||
const restored = loadStoredSession()
|
||||
const operator = ref(restored?.user.account ?? '')
|
||||
const displayName = ref(restored?.user.displayName ?? '')
|
||||
const role = ref<AuthenticatedUser['role'] | ''>(restored?.user.role ?? '')
|
||||
const token = ref(restored?.token ?? '')
|
||||
const expiresAt = ref(restored?.expiresAt ?? '')
|
||||
const currentUser = ref<AuthenticatedUser | null>(restored?.user ?? null)
|
||||
const bootstrapped = ref(false)
|
||||
|
||||
const isAuthenticated = computed(() => Boolean(token.value))
|
||||
setAuthToken(token.value)
|
||||
|
||||
function login(name: string) {
|
||||
operator.value = name || 'ops-admin'
|
||||
token.value = 'local-session'
|
||||
const isAuthenticated = computed(() => Boolean(token.value && currentUser.value))
|
||||
const mustChangePassword = computed(() =>
|
||||
Boolean(currentUser.value?.mustChangePassword),
|
||||
)
|
||||
const roleLabel = computed(() => {
|
||||
if (role.value === 'super_admin') {
|
||||
return '超级管理员'
|
||||
}
|
||||
|
||||
function logout() {
|
||||
if (role.value === 'member') {
|
||||
return '普通成员'
|
||||
}
|
||||
|
||||
return '未登录'
|
||||
})
|
||||
|
||||
async function bootstrap(): Promise<boolean> {
|
||||
if (bootstrapped.value) {
|
||||
return isAuthenticated.value
|
||||
}
|
||||
|
||||
bootstrapped.value = true
|
||||
|
||||
if (!token.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await devopsApi.getCurrentUser()
|
||||
applyUser(user)
|
||||
persist()
|
||||
return true
|
||||
} catch {
|
||||
clearSession()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function login(payload: LoginPayload) {
|
||||
const result = await devopsApi.login(payload)
|
||||
token.value = result.token
|
||||
expiresAt.value = result.expiresAt
|
||||
setAuthToken(result.token)
|
||||
applyUser(result.user)
|
||||
persist()
|
||||
bootstrapped.value = true
|
||||
return result.user
|
||||
}
|
||||
|
||||
async function changePassword(payload: ChangePasswordPayload) {
|
||||
const user = await devopsApi.changePassword(payload)
|
||||
applyUser(user)
|
||||
persist()
|
||||
return user
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
if (token.value) {
|
||||
try {
|
||||
await devopsApi.logout()
|
||||
} catch {
|
||||
// 退出以清理本地登录态为准,后端当前无状态 token 无需强依赖 logout 成功。
|
||||
}
|
||||
}
|
||||
|
||||
clearSession()
|
||||
}
|
||||
|
||||
function applyUser(user: AuthenticatedUser) {
|
||||
currentUser.value = user
|
||||
operator.value = user.account
|
||||
displayName.value = user.displayName
|
||||
role.value = user.role
|
||||
}
|
||||
|
||||
function persist() {
|
||||
if (!token.value || !currentUser.value) {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
return
|
||||
}
|
||||
|
||||
const stored: StoredSession = {
|
||||
token: token.value,
|
||||
expiresAt: expiresAt.value,
|
||||
user: currentUser.value,
|
||||
}
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(stored))
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
operator.value = ''
|
||||
displayName.value = ''
|
||||
role.value = ''
|
||||
token.value = ''
|
||||
expiresAt.value = ''
|
||||
currentUser.value = null
|
||||
setAuthToken()
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
|
||||
return {
|
||||
operator,
|
||||
displayName,
|
||||
role,
|
||||
roleLabel,
|
||||
token,
|
||||
expiresAt,
|
||||
currentUser,
|
||||
isAuthenticated,
|
||||
mustChangePassword,
|
||||
bootstrap,
|
||||
login,
|
||||
changePassword,
|
||||
logout,
|
||||
}
|
||||
})
|
||||
|
||||
function loadStoredSession(): StoredSession | null {
|
||||
try {
|
||||
const rawValue = localStorage.getItem(STORAGE_KEY)
|
||||
|
||||
if (!rawValue) {
|
||||
return null
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(rawValue) as StoredSession
|
||||
|
||||
if (!parsed.token || !parsed.user?.account) {
|
||||
return null
|
||||
}
|
||||
|
||||
return parsed
|
||||
} catch {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
+31
-1
@@ -31,6 +31,36 @@ export type AgentPurpose =
|
||||
| 'release-note'
|
||||
| 'incident-review'
|
||||
|
||||
export type AuthRole = 'super_admin' | 'member'
|
||||
|
||||
export type AuthStatus = 'active' | 'disabled'
|
||||
|
||||
export interface AuthenticatedUser {
|
||||
id: string
|
||||
account: string
|
||||
displayName: string
|
||||
role: AuthRole
|
||||
status: AuthStatus
|
||||
mustChangePassword: boolean
|
||||
passwordVersion: number
|
||||
}
|
||||
|
||||
export interface LoginPayload {
|
||||
account: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
token: string
|
||||
expiresAt: string
|
||||
user: AuthenticatedUser
|
||||
}
|
||||
|
||||
export interface ChangePasswordPayload {
|
||||
currentPassword: string
|
||||
newPassword: string
|
||||
}
|
||||
|
||||
export type IntegrationConfigKey =
|
||||
| 'jenkins'
|
||||
| 'gitea'
|
||||
@@ -221,7 +251,7 @@ export interface CreateDeployRunPayload {
|
||||
projectKey: string
|
||||
environment: EnvironmentName
|
||||
ref: string
|
||||
operator: string
|
||||
operator?: string
|
||||
remark?: string
|
||||
idempotencyKey?: string
|
||||
}
|
||||
|
||||
+157
-13
@@ -1,22 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 登录页对接后端真实鉴权,并处理普通成员首次登录强制改密。
|
||||
*/
|
||||
import { Lock, Monitor } from '@element-plus/icons-vue'
|
||||
import { reactive } from 'vue'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useSessionStore } from '../stores/session'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const session = useSessionStore()
|
||||
const loading = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
operator: 'ops-admin',
|
||||
const loginForm = reactive({
|
||||
account: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const passwordForm = reactive({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
|
||||
const redirect = computed(() =>
|
||||
typeof route.query.redirect === 'string' ? route.query.redirect : '/overview',
|
||||
)
|
||||
|
||||
async function submitLogin() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const user = await session.login(loginForm)
|
||||
passwordForm.currentPassword = loginForm.password
|
||||
|
||||
if (user.mustChangePassword) {
|
||||
ElMessage.warning('首次登录请先修改密码')
|
||||
return
|
||||
}
|
||||
|
||||
await router.push(redirect.value)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '登录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPasswordChange() {
|
||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||
ElMessage.error('两次输入的新密码不一致')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
await session.changePassword({
|
||||
currentPassword: passwordForm.currentPassword,
|
||||
newPassword: passwordForm.newPassword,
|
||||
})
|
||||
ElMessage.success('密码已更新')
|
||||
await router.push(redirect.value)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '修改密码失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function logoutForAccountSwitch() {
|
||||
await session.logout()
|
||||
loginForm.password = ''
|
||||
passwordForm.currentPassword = ''
|
||||
passwordForm.newPassword = ''
|
||||
passwordForm.confirmPassword = ''
|
||||
}
|
||||
|
||||
function submit() {
|
||||
session.login(form.operator)
|
||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/overview'
|
||||
void router.push(redirect)
|
||||
if (session.mustChangePassword) {
|
||||
void submitPasswordChange()
|
||||
return
|
||||
}
|
||||
|
||||
void submitLogin()
|
||||
}
|
||||
|
||||
function accountLabel() {
|
||||
return session.displayName || session.operator
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -26,24 +98,78 @@ function submit() {
|
||||
<div class="login-copy">
|
||||
<ElIcon><Monitor /></ElIcon>
|
||||
<h1>DevOps 运维平台</h1>
|
||||
<p>当前使用本地操作员会话进入控制台;真实身份、RBAC 和审计由后端认证模块接入。</p>
|
||||
<p>使用平台账号进入控制台。</p>
|
||||
</div>
|
||||
<ElForm class="login-form" label-position="top" @submit.prevent="submit">
|
||||
<ElFormItem label="操作员">
|
||||
<ElInput v-model="form.operator" size="large" autocomplete="username" />
|
||||
<template v-if="!session.mustChangePassword">
|
||||
<ElFormItem label="账号">
|
||||
<ElInput v-model="loginForm.account" size="large" autocomplete="username" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="口令">
|
||||
<ElFormItem label="密码">
|
||||
<ElInput
|
||||
v-model="form.password"
|
||||
v-model="loginForm.password"
|
||||
size="large"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElButton type="primary" size="large" :icon="Lock" native-type="submit">
|
||||
进入控制台
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="large"
|
||||
:icon="Lock"
|
||||
:loading="loading"
|
||||
native-type="submit"
|
||||
>
|
||||
登录
|
||||
</ElButton>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="change-title">
|
||||
<strong>{{ accountLabel() }}</strong>
|
||||
<span>需要更新密码</span>
|
||||
</div>
|
||||
<ElFormItem label="当前密码">
|
||||
<ElInput
|
||||
v-model="passwordForm.currentPassword"
|
||||
size="large"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="新密码">
|
||||
<ElInput
|
||||
v-model="passwordForm.newPassword"
|
||||
size="large"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="确认新密码">
|
||||
<ElInput
|
||||
v-model="passwordForm.confirmPassword"
|
||||
size="large"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="large"
|
||||
:icon="Lock"
|
||||
:loading="loading"
|
||||
native-type="submit"
|
||||
>
|
||||
更新并进入
|
||||
</ElButton>
|
||||
<ElButton class="switch-account" text @click="logoutForAccountSwitch">
|
||||
切换账号
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElForm>
|
||||
</section>
|
||||
</main>
|
||||
@@ -103,6 +229,24 @@ function submit() {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.change-title {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin-bottom: 18px;
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.change-title span {
|
||||
color: #697386;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.switch-account {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.login-panel {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 发布中心负责创建发布单、同步 Jenkins 状态并展示当前运行流程。
|
||||
*/
|
||||
import { CircleClose, Promotion, RefreshRight, VideoPause } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 运行记录页展示历史发布单、Jenkins 同步入口和后端步骤流转图。
|
||||
*/
|
||||
import { RefreshRight } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 系统配置页展示集成变量说明、配置边界和审计日志脱敏 JSON。
|
||||
*/
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import AuditJsonCell from '../components/AuditJsonCell.vue'
|
||||
import StatusPill from '../components/StatusPill.vue'
|
||||
@@ -68,8 +71,26 @@ function auditResource(record: AuditLogEntry): string {
|
||||
: record.resourceType
|
||||
}
|
||||
|
||||
function hasAuditRecord(value?: Record<string, unknown>): value is Record<string, unknown> {
|
||||
return Boolean(value && Object.keys(value).length > 0)
|
||||
}
|
||||
|
||||
function auditPayload(record: AuditLogEntry): unknown {
|
||||
return record.after ?? record.parameterDigest ?? record.before ?? null
|
||||
const payload: Record<string, unknown> = {}
|
||||
|
||||
if (hasAuditRecord(record.parameterDigest)) {
|
||||
payload.parameterDigest = record.parameterDigest
|
||||
}
|
||||
|
||||
if (hasAuditRecord(record.before)) {
|
||||
payload.before = record.before
|
||||
}
|
||||
|
||||
if (hasAuditRecord(record.after)) {
|
||||
payload.after = record.after
|
||||
}
|
||||
|
||||
return Object.keys(payload).length > 0 ? payload : null
|
||||
}
|
||||
|
||||
function variableGroups(item: IntegrationConfigItem): VariableGroup[] {
|
||||
|
||||
Reference in New Issue
Block a user