Skip to content

Commit e18ee03

Browse files
committed
feat(frontend): add team settings component for management configuration
1 parent 3a11777 commit e18ee03

File tree

2 files changed

+278
-0
lines changed

2 files changed

+278
-0
lines changed
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
import { useSettingsForm } from '@/composables/useSettingsForm'
4+
import type { SettingsComponentProps, SettingsComponentEvents } from '@/composables/useSettingsComponentRegistry'
5+
import { DsCard } from '@/components/ui/ds-card'
6+
import { Button } from '@/components/ui/button'
7+
import { Spinner } from '@/components/ui/spinner'
8+
import { Input } from '@/components/ui/input'
9+
import { Checkbox } from '@/components/ui/checkbox'
10+
import { Label } from '@/components/ui/label'
11+
import { getEnv } from '@/utils/env'
12+
13+
const props = defineProps<SettingsComponentProps>()
14+
const emit = defineEmits<SettingsComponentEvents>()
15+
16+
const apiUrl = getEnv('VITE_DEPLOYSTACK_BACKEND_URL') || ''
17+
18+
// Use the settings form composable
19+
const {
20+
formValues,
21+
updateField,
22+
getFieldError
23+
} = useSettingsForm(props.settings)
24+
25+
// Separate loading states for each card
26+
const isSavingCard1 = ref(false)
27+
const isSavingCard2 = ref(false)
28+
const isSavingCard3 = ref(false)
29+
30+
// Get setting by key
31+
function getSetting(key: string) {
32+
return props.settings.find(s => s.key === key)
33+
}
34+
35+
// Save specific settings by keys
36+
async function saveSpecificSettings(keys: string[], cardNumber: number): Promise<boolean> {
37+
const loadingRef = cardNumber === 1 ? isSavingCard1 : cardNumber === 2 ? isSavingCard2 : isSavingCard3
38+
loadingRef.value = true
39+
40+
try {
41+
const settingsToUpdate = keys.map(key => {
42+
const setting = props.settings.find(s => s.key === key)
43+
return {
44+
key,
45+
value: formValues.value[key],
46+
type: setting?.type,
47+
group_id: setting?.group_id,
48+
description: setting?.description,
49+
encrypted: setting?.is_encrypted || false
50+
}
51+
})
52+
53+
const response = await fetch(`${apiUrl}/api/settings/bulk`, {
54+
method: 'POST',
55+
headers: {
56+
'Content-Type': 'application/json',
57+
},
58+
credentials: 'include',
59+
body: JSON.stringify({ settings: settingsToUpdate }),
60+
})
61+
62+
if (!response.ok) {
63+
const errorData = await response.json().catch(() => ({}))
64+
throw new Error(errorData.error || errorData.message || `Failed to save settings: ${response.statusText}`)
65+
}
66+
67+
const result = await response.json()
68+
if (!result.success) {
69+
throw new Error(result.message || 'Failed to save settings')
70+
}
71+
72+
return true
73+
} catch (error) {
74+
console.error('Failed to save settings:', error)
75+
return false
76+
} finally {
77+
loadingRef.value = false
78+
}
79+
}
80+
81+
// Handle form submission for Card 1
82+
async function handleSaveCard1() {
83+
const success = await saveSpecificSettings(['team.default_member_limit', 'team.creation_limit'], 1)
84+
if (success) {
85+
emit('settings-updated', props.settings)
86+
}
87+
}
88+
89+
// Handle form submission for Card 2
90+
async function handleSaveCard2() {
91+
const success = await saveSpecificSettings(['team.default_mcp_server_limit', 'team.default_non_http_mcp_limit'], 2)
92+
if (success) {
93+
emit('settings-updated', props.settings)
94+
}
95+
}
96+
97+
// Handle form submission for Card 3
98+
async function handleSaveCard3() {
99+
const success = await saveSpecificSettings(['team.allow_remote_mcp'], 3)
100+
if (success) {
101+
emit('settings-updated', props.settings)
102+
}
103+
}
104+
</script>
105+
106+
<template>
107+
<div class="space-y-6">
108+
<!-- Card 1: Team & Member Limits -->
109+
<DsCard title="Team & Member Limits">
110+
<p class="text-sm text-muted-foreground mb-6">
111+
Configure default limits for team creation and member capacity.
112+
</p>
113+
114+
<div class="space-y-6">
115+
<!-- Default Team Member Limit -->
116+
<div class="space-y-2">
117+
<Label for="team-member-limit">
118+
{{ getSetting('team.default_member_limit')?.name || 'Default Team Member Limit' }}
119+
</Label>
120+
<Input
121+
id="team-member-limit"
122+
type="number"
123+
:model-value="String(formValues['team.default_member_limit'] || '')"
124+
@update:model-value="(value) => updateField('team.default_member_limit', Number(value))"
125+
placeholder="3"
126+
:class="{ 'border-destructive': getFieldError('team.default_member_limit') }"
127+
/>
128+
<p v-if="getFieldError('team.default_member_limit')" class="text-sm text-destructive">
129+
{{ getFieldError('team.default_member_limit') }}
130+
</p>
131+
<p class="text-xs text-muted-foreground">
132+
{{ getSetting('team.default_member_limit')?.description }}
133+
</p>
134+
</div>
135+
136+
<!-- Team Creation Limit -->
137+
<div class="space-y-2">
138+
<Label for="team-creation-limit">
139+
{{ getSetting('team.creation_limit')?.name || 'Team Creation Limit' }}
140+
</Label>
141+
<Input
142+
id="team-creation-limit"
143+
type="number"
144+
:model-value="String(formValues['team.creation_limit'] || '')"
145+
@update:model-value="(value) => updateField('team.creation_limit', Number(value))"
146+
placeholder="3"
147+
:class="{ 'border-destructive': getFieldError('team.creation_limit') }"
148+
/>
149+
<p v-if="getFieldError('team.creation_limit')" class="text-sm text-destructive">
150+
{{ getFieldError('team.creation_limit') }}
151+
</p>
152+
<p class="text-xs text-muted-foreground">
153+
{{ getSetting('team.creation_limit')?.description }}
154+
</p>
155+
</div>
156+
</div>
157+
158+
<template #footer-actions>
159+
<Button
160+
:disabled="isSavingCard1"
161+
@click="handleSaveCard1"
162+
>
163+
<Spinner v-if="isSavingCard1" class="mr-2" />
164+
Save Changes
165+
</Button>
166+
</template>
167+
</DsCard>
168+
169+
<!-- Card 2: MCP Server Limits -->
170+
<DsCard title="MCP Server Limits">
171+
<p class="text-sm text-muted-foreground mb-6">
172+
Configure default limits for MCP server installations per team.
173+
</p>
174+
175+
<div class="space-y-6">
176+
<!-- Default MCP Server Limit -->
177+
<div class="space-y-2">
178+
<Label for="mcp-server-limit">
179+
{{ getSetting('team.default_mcp_server_limit')?.name || 'Default MCP Server Limit' }}
180+
</Label>
181+
<Input
182+
id="mcp-server-limit"
183+
type="number"
184+
:model-value="String(formValues['team.default_mcp_server_limit'] || '')"
185+
@update:model-value="(value) => updateField('team.default_mcp_server_limit', Number(value))"
186+
placeholder="5"
187+
:class="{ 'border-destructive': getFieldError('team.default_mcp_server_limit') }"
188+
/>
189+
<p v-if="getFieldError('team.default_mcp_server_limit')" class="text-sm text-destructive">
190+
{{ getFieldError('team.default_mcp_server_limit') }}
191+
</p>
192+
<p class="text-xs text-muted-foreground">
193+
{{ getSetting('team.default_mcp_server_limit')?.description }}
194+
</p>
195+
</div>
196+
197+
<!-- Default Non-HTTP MCP Limit -->
198+
<div class="space-y-2">
199+
<Label for="non-http-mcp-limit">
200+
{{ getSetting('team.default_non_http_mcp_limit')?.name || 'Default Non-HTTP MCP Limit' }}
201+
</Label>
202+
<Input
203+
id="non-http-mcp-limit"
204+
type="number"
205+
:model-value="String(formValues['team.default_non_http_mcp_limit'] || '')"
206+
@update:model-value="(value) => updateField('team.default_non_http_mcp_limit', Number(value))"
207+
placeholder="1"
208+
:class="{ 'border-destructive': getFieldError('team.default_non_http_mcp_limit') }"
209+
/>
210+
<p v-if="getFieldError('team.default_non_http_mcp_limit')" class="text-sm text-destructive">
211+
{{ getFieldError('team.default_non_http_mcp_limit') }}
212+
</p>
213+
<p class="text-xs text-muted-foreground">
214+
{{ getSetting('team.default_non_http_mcp_limit')?.description }}
215+
</p>
216+
</div>
217+
</div>
218+
219+
<template #footer-actions>
220+
<Button
221+
:disabled="isSavingCard2"
222+
@click="handleSaveCard2"
223+
>
224+
<Spinner v-if="isSavingCard2" class="mr-2" />
225+
Save Changes
226+
</Button>
227+
</template>
228+
</DsCard>
229+
230+
<!-- Card 3: Remote MCP Options -->
231+
<DsCard title="Remote MCP Options">
232+
<p class="text-sm text-muted-foreground mb-6">
233+
Control whether teams can install MCP servers from external sources.
234+
</p>
235+
236+
<div class="space-y-6">
237+
<!-- Allow Remote MCP Checkbox -->
238+
<div class="flex items-start gap-3">
239+
<Checkbox
240+
id="allow-remote-mcp"
241+
:checked="Boolean(formValues['team.allow_remote_mcp'])"
242+
@update:checked="(value: boolean) => updateField('team.allow_remote_mcp', value)"
243+
/>
244+
<div class="grid gap-1">
245+
<label
246+
for="allow-remote-mcp"
247+
class="cursor-pointer text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
248+
>
249+
{{ getSetting('team.allow_remote_mcp')?.name || 'Allow Remote MCP Servers' }}
250+
</label>
251+
<p class="text-muted-foreground text-sm">
252+
{{ getSetting('team.allow_remote_mcp')?.description }}
253+
</p>
254+
</div>
255+
</div>
256+
</div>
257+
258+
<template #footer-actions>
259+
<Button
260+
:disabled="isSavingCard3"
261+
@click="handleSaveCard3"
262+
>
263+
<Spinner v-if="isSavingCard3" class="mr-2" />
264+
Save Changes
265+
</Button>
266+
</template>
267+
</DsCard>
268+
</div>
269+
</template>

services/frontend/src/components/globalSettings/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { registerSettingsComponent, getAllRegisteredComponents } from '@/composables/useSettingsComponentRegistry'
22
import GitHubAppSettings from './GitHubAppSettings.vue'
33
import SmtpSettings from './SmtpSettings.vue'
4+
import TeamSettings from './TeamSettings.vue'
45

56
/**
67
* Register all custom settings components
@@ -22,6 +23,14 @@ export function registerSettingsComponents() {
2223
author: 'DeployStack Team',
2324
version: '1.0.0'
2425
})
26+
27+
// Register Team Settings Component
28+
registerSettingsComponent('team', {
29+
component: TeamSettings,
30+
description: 'Team management and limit configuration',
31+
author: 'DeployStack Team',
32+
version: '1.0.0'
33+
})
2534
}
2635

2736
/**

0 commit comments

Comments
 (0)