Compare commits

...

2 Commits

Author SHA1 Message Date
hyung-hwan d4e0ea811f added tags to ssh server group 2026-06-11 12:38:36 +09:00
hyung-hwan c9fba01ef1 renders the server tags in chips 2026-06-11 11:39:25 +09:00
9 changed files with 83 additions and 40 deletions
+18 -5
View File
@@ -9,7 +9,12 @@ import "codit/internal/models"
import "codit/internal/util"
func scanSSHServerGroup(row interface{ Scan(dest ...any) error }, item *models.SSHServerGroup) error {
return row.Scan(&item.ID, &item.Name, &item.Description, &item.Enabled, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
var tagsJSON string
var err error
err = row.Scan(&item.ID, &item.Name, &item.Description, &item.Enabled, &tagsJSON, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
if err != nil { return err }
item.Tags, err = decodeStringList(tagsJSON)
return err
}
func (s *Store) listSSHServerGroupMemberIDs(groupID string) ([]string, error) {
@@ -42,7 +47,7 @@ func (s *Store) ListSSHServerGroups() ([]models.SSHServerGroup, error) {
var item models.SSHServerGroup
var err error
rows, err = s.Query(`SELECT public_id, name, description, enabled, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_server_groups ORDER BY name`)
rows, err = s.Query(`SELECT public_id, name, description, enabled, tags_json, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_server_groups ORDER BY name`)
if err != nil { return nil, err }
defer rows.Close()
for rows.Next() {
@@ -62,7 +67,7 @@ func (s *Store) GetSSHServerGroup(id string) (models.SSHServerGroup, error) {
var item models.SSHServerGroup
var err error
row = s.QueryRow(`SELECT public_id, name, description, enabled, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_server_groups WHERE public_id = ?`, strings.TrimSpace(id))
row = s.QueryRow(`SELECT public_id, name, description, enabled, tags_json, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_server_groups WHERE public_id = ?`, strings.TrimSpace(id))
err = scanSSHServerGroup(row, &item)
if err != nil { return item, err }
item.ServerIDs, err = s.listSSHServerGroupMemberIDs(item.ID)
@@ -104,17 +109,21 @@ func (s *Store) CreateSSHServerGroup(item models.SSHServerGroup) (models.SSHServ
var err error
var now int64
var tagsJSON string
if strings.TrimSpace(item.Name) == "" { return item, errors.New("name is required") }
if strings.TrimSpace(item.ID) == "" {
item.ID, err = util.NewID()
if err != nil { return item, err }
}
item.Tags = normalizeStringList(item.Tags)
tagsJSON, err = encodeStringList(item.Tags)
if err != nil { return item, err }
now = time.Now().UTC().Unix()
item.CreatedAt = now
item.UpdatedAt = now
tx, owned, err = s.begin()
if err != nil { return item, err }
_, err = tx.Exec(`INSERT INTO ssh_server_groups (public_id, name, description, enabled, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, item.ID, strings.TrimSpace(item.Name), strings.TrimSpace(item.Description), item.Enabled, strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedBySubjectName), item.CreatedAt, item.UpdatedAt)
_, err = tx.Exec(`INSERT INTO ssh_server_groups (public_id, name, description, enabled, tags_json, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, item.ID, strings.TrimSpace(item.Name), strings.TrimSpace(item.Description), item.Enabled, tagsJSON, strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedBySubjectName), item.CreatedAt, item.UpdatedAt)
if err != nil { rollbackIfOwned(tx, owned); return item, err }
err = commitIfOwned(tx, owned)
if err != nil { return item, err }
@@ -127,11 +136,15 @@ func (s *Store) UpdateSSHServerGroup(item models.SSHServerGroup) (models.SSHServ
var err error
var now int64
var tagsJSON string
if strings.TrimSpace(item.Name) == "" { return item, errors.New("name is required") }
item.Tags = normalizeStringList(item.Tags)
tagsJSON, err = encodeStringList(item.Tags)
if err != nil { return item, err }
now = time.Now().UTC().Unix()
tx, owned, err = s.begin()
if err != nil { return item, err }
_, err = tx.Exec(`UPDATE ssh_server_groups SET name = ?, description = ?, enabled = ?, updated_at = ? WHERE public_id = ?`, strings.TrimSpace(item.Name), strings.TrimSpace(item.Description), item.Enabled, now, strings.TrimSpace(item.ID))
_, err = tx.Exec(`UPDATE ssh_server_groups SET name = ?, description = ?, enabled = ?, tags_json = ?, updated_at = ? WHERE public_id = ?`, strings.TrimSpace(item.Name), strings.TrimSpace(item.Description), item.Enabled, tagsJSON, now, strings.TrimSpace(item.ID))
if err != nil { rollbackIfOwned(tx, owned); return item, err }
err = commitIfOwned(tx, owned)
if err != nil { return item, err }
+3 -1
View File
@@ -73,6 +73,7 @@ type sshServerGroupRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
Tags []string `json:"tags"`
}
type sshServerGroupMemberRequest struct {
@@ -471,7 +472,7 @@ func (api *API) CreateSSHServerGroupAdmin(w http.ResponseWriter, r *http.Request
w.WriteHeader(http.StatusUnauthorized)
return
}
item = models.SSHServerGroup{Name: strings.TrimSpace(req.Name), Description: strings.TrimSpace(req.Description), Enabled: req.Enabled, CreatedByKind: "user", CreatedBySubjectID: user.ID, CreatedBySubjectName: user.Username}
item = models.SSHServerGroup{Name: strings.TrimSpace(req.Name), Description: strings.TrimSpace(req.Description), Enabled: req.Enabled, Tags: req.Tags, CreatedByKind: "user", CreatedBySubjectID: user.ID, CreatedBySubjectName: user.Username}
item, err = api.store(r).CreateSSHServerGroup(item)
if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
@@ -498,6 +499,7 @@ func (api *API) UpdateSSHServerGroupAdmin(w http.ResponseWriter, r *http.Request
item.Name = strings.TrimSpace(req.Name)
item.Description = strings.TrimSpace(req.Description)
item.Enabled = req.Enabled
item.Tags = req.Tags
item, err = api.store(r).UpdateSSHServerGroup(item)
if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
+1
View File
@@ -551,6 +551,7 @@ type SSHServerGroup struct {
Name string `json:"name"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
Tags []string `json:"tags"`
ServerIDs []string `json:"server_ids"`
Servers []SSHServer `json:"servers"`
CreatedByKind string `json:"created_by_kind"`
@@ -0,0 +1 @@
ALTER TABLE ssh_server_groups ADD COLUMN tags_json TEXT NOT NULL DEFAULT '[]';
+2
View File
@@ -936,6 +936,7 @@ export interface SSHServerGroup {
name: string
description: string
enabled: boolean
tags: string[]
server_ids: string[]
servers?: SSHServer[]
created_by_kind: string
@@ -1071,6 +1072,7 @@ export type SSHServerGroupUpsertPayload = {
name: string
description: string
enabled: boolean
tags: string[]
}
export type PKIClientProfileTargetPayload = {
@@ -79,6 +79,7 @@ export default function SSHServerGroupDetailsDialog(props: SSHServerGroupDetails
<TextField label="Group ID" value={item?.id || ''} InputProps={{ readOnly: true }} />
<TextField label="Description" value={item?.description || ''} InputProps={{ readOnly: true }} multiline minRows={2} />
<TextField label="Status" value={item?.enabled ? 'Enabled' : 'Disabled'} InputProps={{ readOnly: true }} />
<TextField label="Tags" value={(item?.tags || []).join(', ') || '-'} InputProps={{ readOnly: true }} />
{showAdminFields ? <TextField label="Created By Kind" value={item?.created_by_kind || '-'} InputProps={{ readOnly: true }} /> : null}
{showAdminFields ? <TextField label="Created By Subject ID" value={item?.created_by_subject_id || '-'} InputProps={{ readOnly: true }} /> : null}
{showAdminFields ? <TextField label="Created By Subject Name" value={item?.created_by_subject_name || '-'} InputProps={{ readOnly: true }} /> : null}
@@ -14,6 +14,7 @@ export type SSHServerGroupFormState = {
name: string
description: string
enabled: boolean
tagsText: string
}
type SSHServerGroupFormDialogProps = {
@@ -49,6 +50,12 @@ export default function SSHServerGroupFormDialog(props: SSHServerGroupFormDialog
multiline
minRows={2}
/>
<TextField
label="Tags"
value={props.form.tagsText}
onChange={(event) => props.setForm((prev) => ({ ...prev, tagsText: event.target.value }))}
helperText="Comma-separated list of tags"
/>
<FormControlLabel
control={<Checkbox checked={props.form.enabled} onChange={(event) => props.setForm((prev) => ({ ...prev, enabled: event.target.checked }))} />}
label="Enabled"
+47 -31
View File
@@ -35,7 +35,8 @@ const emptyForm = (): SSHServerFormState => ({
const emptyGroupForm = (): SSHServerGroupFormState => ({
name: '',
description: '',
enabled: true
enabled: true,
tagsText: ''
})
function fmt(value: number): string {
@@ -161,6 +162,16 @@ export default function AdminSSHServersPage() {
setDialogOpen(true)
}
const parseTags = (value: string) =>
Array.from(
new Set(
value
.split(',')
.map((item) => item.trim())
.filter((item) => item !== '')
)
)
const openGroupCreate = () => {
setEditingGroupID(null)
setGroupForm(emptyGroupForm())
@@ -173,7 +184,8 @@ export default function AdminSSHServersPage() {
setGroupForm({
name: item.name,
description: item.description || '',
enabled: item.enabled
enabled: item.enabled,
tagsText: (item.tags || []).join(', ')
})
setGroupDialogError(null)
setGroupDialogOpen(true)
@@ -183,7 +195,8 @@ export default function AdminSSHServersPage() {
const payload: SSHServerGroupUpsertPayload = {
name: groupForm.name.trim(),
description: groupForm.description.trim(),
enabled: groupForm.enabled
enabled: groupForm.enabled,
tags: parseTags(groupForm.tagsText)
}
if (!payload.name) {
setGroupDialogError('Name is required')
@@ -298,16 +311,6 @@ export default function AdminSSHServersPage() {
setDialogOpen(true)
}
const parseTags = (value: string) =>
Array.from(
new Set(
value
.split(',')
.map((item) => item.trim())
.filter((item) => item !== '')
)
)
const handleSave = async () => {
const payload: SSHServerUpsertPayload = {
name: form.name.trim(),
@@ -493,26 +496,36 @@ export default function AdminSSHServersPage() {
<Typography component="div">
{item.name} ({item.id})
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
{(item.tags || []).map((tag: string) => (
<Chip key={tag} label={tag} size="small" sx={{ ml: 0.5 }} />
))}
</Typography>
}
secondary={
<Typography variant="caption" color="text.secondary">
{item.host}:{item.port}· Tags: {(item.tags || []).join(', ') || '-'}· Updated: {fmt(item.updated_at)}
<br />
Groups: {serverGroups.length ? serverGroups.map((group: SSHServerGroup, index: number) => (
<Box key={group.id} component="span">
{index > 0 ? ',' : ''}
<Link
underline="hover"
onClick={() => void openGroupView(group)}
sx={{ cursor: 'default' }}
>
{group.name}
</Link>
</Box>
)) : '-'}
{item.description? (<><br />{item.description}</>): null}
</Typography>
<Box sx={{ display: 'grid', gap: 0.25 }}>
<Typography variant="caption" color="text.secondary">
{item.host}:{item.port} · Updated: {fmt(item.updated_at)}
</Typography>
<Typography variant="caption" color="text.secondary">
Groups: {serverGroups.length ? serverGroups.map((group: SSHServerGroup, index: number) => (
<Box key={group.id} component="span">
{index > 0 ? ',' : ''}
<Link
underline="hover"
onClick={() => void openGroupView(group)}
sx={{ cursor: 'default' }}
>
{group.name}
</Link>
</Box>
)) : '-'}
</Typography>
{item.description ? (
<Typography variant="caption" color="text.secondary">
{item.description}
</Typography>
) : null}
</Box>
}
/>
<ListRowActions>
@@ -563,11 +576,14 @@ export default function AdminSSHServersPage() {
<Typography component="div">
{item.name} ({item.id})
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
{(item.tags || []).map((tag: string) => (
<Chip key={tag} label={tag} size="small" sx={{ ml: 0.5 }} />
))}
</Typography>
}
secondary={
<Typography variant="caption" color="text.secondary">
{(item.server_ids || []).length} servers · {item.description || '-'}
{(item.server_ids || []).length} servers · {item.description || '-'}
</Typography>
}
/>
+3 -3
View File
@@ -758,6 +758,9 @@ export default function SSHServersPage() {
<Typography component="div">
{item.name} ({item.id})
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
{(item.tags || []).map((tag: string) => (
<Chip key={tag} label={tag} size="small" variant="outlined" sx={{ ml: 0.5 }} />
))}
</Typography>
}
secondary={
@@ -765,9 +768,6 @@ export default function SSHServersPage() {
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
{item.host}:{item.port}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
Tags: {(item.tags || []).join(', ') || '-'}
</Typography>
{item.description? (
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
{item.description}