Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4e0ea811f | |||
| c9fba01ef1 |
@@ -9,7 +9,12 @@ import "codit/internal/models"
|
|||||||
import "codit/internal/util"
|
import "codit/internal/util"
|
||||||
|
|
||||||
func scanSSHServerGroup(row interface{ Scan(dest ...any) error }, item *models.SSHServerGroup) error {
|
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) {
|
func (s *Store) listSSHServerGroupMemberIDs(groupID string) ([]string, error) {
|
||||||
@@ -42,7 +47,7 @@ func (s *Store) ListSSHServerGroups() ([]models.SSHServerGroup, error) {
|
|||||||
var item models.SSHServerGroup
|
var item models.SSHServerGroup
|
||||||
var err error
|
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 }
|
if err != nil { return nil, err }
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
@@ -62,7 +67,7 @@ func (s *Store) GetSSHServerGroup(id string) (models.SSHServerGroup, error) {
|
|||||||
var item models.SSHServerGroup
|
var item models.SSHServerGroup
|
||||||
var err error
|
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)
|
err = scanSSHServerGroup(row, &item)
|
||||||
if err != nil { return item, err }
|
if err != nil { return item, err }
|
||||||
item.ServerIDs, err = s.listSSHServerGroupMemberIDs(item.ID)
|
item.ServerIDs, err = s.listSSHServerGroupMemberIDs(item.ID)
|
||||||
@@ -104,17 +109,21 @@ func (s *Store) CreateSSHServerGroup(item models.SSHServerGroup) (models.SSHServ
|
|||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
|
|
||||||
|
var tagsJSON string
|
||||||
if strings.TrimSpace(item.Name) == "" { return item, errors.New("name is required") }
|
if strings.TrimSpace(item.Name) == "" { return item, errors.New("name is required") }
|
||||||
if strings.TrimSpace(item.ID) == "" {
|
if strings.TrimSpace(item.ID) == "" {
|
||||||
item.ID, err = util.NewID()
|
item.ID, err = util.NewID()
|
||||||
if err != nil { return item, err }
|
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()
|
now = time.Now().UTC().Unix()
|
||||||
item.CreatedAt = now
|
item.CreatedAt = now
|
||||||
item.UpdatedAt = now
|
item.UpdatedAt = now
|
||||||
tx, owned, err = s.begin()
|
tx, owned, err = s.begin()
|
||||||
if err != nil { return item, err }
|
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 }
|
if err != nil { rollbackIfOwned(tx, owned); return item, err }
|
||||||
err = commitIfOwned(tx, owned)
|
err = commitIfOwned(tx, owned)
|
||||||
if err != nil { return item, err }
|
if err != nil { return item, err }
|
||||||
@@ -127,11 +136,15 @@ func (s *Store) UpdateSSHServerGroup(item models.SSHServerGroup) (models.SSHServ
|
|||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
|
|
||||||
|
var tagsJSON string
|
||||||
if strings.TrimSpace(item.Name) == "" { return item, errors.New("name is required") }
|
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()
|
now = time.Now().UTC().Unix()
|
||||||
tx, owned, err = s.begin()
|
tx, owned, err = s.begin()
|
||||||
if err != nil { return item, err }
|
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 }
|
if err != nil { rollbackIfOwned(tx, owned); return item, err }
|
||||||
err = commitIfOwned(tx, owned)
|
err = commitIfOwned(tx, owned)
|
||||||
if err != nil { return item, err }
|
if err != nil { return item, err }
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ type sshServerGroupRequest struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshServerGroupMemberRequest struct {
|
type sshServerGroupMemberRequest struct {
|
||||||
@@ -471,7 +472,7 @@ func (api *API) CreateSSHServerGroupAdmin(w http.ResponseWriter, r *http.Request
|
|||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
return
|
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)
|
item, err = api.store(r).CreateSSHServerGroup(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
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.Name = strings.TrimSpace(req.Name)
|
||||||
item.Description = strings.TrimSpace(req.Description)
|
item.Description = strings.TrimSpace(req.Description)
|
||||||
item.Enabled = req.Enabled
|
item.Enabled = req.Enabled
|
||||||
|
item.Tags = req.Tags
|
||||||
item, err = api.store(r).UpdateSSHServerGroup(item)
|
item, err = api.store(r).UpdateSSHServerGroup(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||||
|
|||||||
@@ -551,6 +551,7 @@ type SSHServerGroup struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
ServerIDs []string `json:"server_ids"`
|
ServerIDs []string `json:"server_ids"`
|
||||||
Servers []SSHServer `json:"servers"`
|
Servers []SSHServer `json:"servers"`
|
||||||
CreatedByKind string `json:"created_by_kind"`
|
CreatedByKind string `json:"created_by_kind"`
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE ssh_server_groups ADD COLUMN tags_json TEXT NOT NULL DEFAULT '[]';
|
||||||
@@ -936,6 +936,7 @@ export interface SSHServerGroup {
|
|||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
tags: string[]
|
||||||
server_ids: string[]
|
server_ids: string[]
|
||||||
servers?: SSHServer[]
|
servers?: SSHServer[]
|
||||||
created_by_kind: string
|
created_by_kind: string
|
||||||
@@ -1071,6 +1072,7 @@ export type SSHServerGroupUpsertPayload = {
|
|||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PKIClientProfileTargetPayload = {
|
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="Group ID" value={item?.id || ''} InputProps={{ readOnly: true }} />
|
||||||
<TextField label="Description" value={item?.description || ''} InputProps={{ readOnly: true }} multiline minRows={2} />
|
<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="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 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 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}
|
{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
|
name: string
|
||||||
description: string
|
description: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
tagsText: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SSHServerGroupFormDialogProps = {
|
type SSHServerGroupFormDialogProps = {
|
||||||
@@ -49,6 +50,12 @@ export default function SSHServerGroupFormDialog(props: SSHServerGroupFormDialog
|
|||||||
multiline
|
multiline
|
||||||
minRows={2}
|
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
|
<FormControlLabel
|
||||||
control={<Checkbox checked={props.form.enabled} onChange={(event) => props.setForm((prev) => ({ ...prev, enabled: event.target.checked }))} />}
|
control={<Checkbox checked={props.form.enabled} onChange={(event) => props.setForm((prev) => ({ ...prev, enabled: event.target.checked }))} />}
|
||||||
label="Enabled"
|
label="Enabled"
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ const emptyForm = (): SSHServerFormState => ({
|
|||||||
const emptyGroupForm = (): SSHServerGroupFormState => ({
|
const emptyGroupForm = (): SSHServerGroupFormState => ({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
tagsText: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
function fmt(value: number): string {
|
function fmt(value: number): string {
|
||||||
@@ -161,6 +162,16 @@ export default function AdminSSHServersPage() {
|
|||||||
setDialogOpen(true)
|
setDialogOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseTags = (value: string) =>
|
||||||
|
Array.from(
|
||||||
|
new Set(
|
||||||
|
value
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter((item) => item !== '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const openGroupCreate = () => {
|
const openGroupCreate = () => {
|
||||||
setEditingGroupID(null)
|
setEditingGroupID(null)
|
||||||
setGroupForm(emptyGroupForm())
|
setGroupForm(emptyGroupForm())
|
||||||
@@ -173,7 +184,8 @@ export default function AdminSSHServersPage() {
|
|||||||
setGroupForm({
|
setGroupForm({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
description: item.description || '',
|
description: item.description || '',
|
||||||
enabled: item.enabled
|
enabled: item.enabled,
|
||||||
|
tagsText: (item.tags || []).join(', ')
|
||||||
})
|
})
|
||||||
setGroupDialogError(null)
|
setGroupDialogError(null)
|
||||||
setGroupDialogOpen(true)
|
setGroupDialogOpen(true)
|
||||||
@@ -183,7 +195,8 @@ export default function AdminSSHServersPage() {
|
|||||||
const payload: SSHServerGroupUpsertPayload = {
|
const payload: SSHServerGroupUpsertPayload = {
|
||||||
name: groupForm.name.trim(),
|
name: groupForm.name.trim(),
|
||||||
description: groupForm.description.trim(),
|
description: groupForm.description.trim(),
|
||||||
enabled: groupForm.enabled
|
enabled: groupForm.enabled,
|
||||||
|
tags: parseTags(groupForm.tagsText)
|
||||||
}
|
}
|
||||||
if (!payload.name) {
|
if (!payload.name) {
|
||||||
setGroupDialogError('Name is required')
|
setGroupDialogError('Name is required')
|
||||||
@@ -298,16 +311,6 @@ export default function AdminSSHServersPage() {
|
|||||||
setDialogOpen(true)
|
setDialogOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseTags = (value: string) =>
|
|
||||||
Array.from(
|
|
||||||
new Set(
|
|
||||||
value
|
|
||||||
.split(',')
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter((item) => item !== '')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const payload: SSHServerUpsertPayload = {
|
const payload: SSHServerUpsertPayload = {
|
||||||
name: form.name.trim(),
|
name: form.name.trim(),
|
||||||
@@ -493,12 +496,17 @@ export default function AdminSSHServersPage() {
|
|||||||
<Typography component="div">
|
<Typography component="div">
|
||||||
{item.name} ({item.id})
|
{item.name} ({item.id})
|
||||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
{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>
|
</Typography>
|
||||||
}
|
}
|
||||||
secondary={
|
secondary={
|
||||||
|
<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">
|
<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) => (
|
Groups: {serverGroups.length ? serverGroups.map((group: SSHServerGroup, index: number) => (
|
||||||
<Box key={group.id} component="span">
|
<Box key={group.id} component="span">
|
||||||
{index > 0 ? ',' : ''}
|
{index > 0 ? ',' : ''}
|
||||||
@@ -511,8 +519,13 @@ export default function AdminSSHServersPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
)) : '-'}
|
)) : '-'}
|
||||||
{item.description? (<><br />{item.description}</>): null}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{item.description ? (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{item.description}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
@@ -563,6 +576,9 @@ export default function AdminSSHServersPage() {
|
|||||||
<Typography component="div">
|
<Typography component="div">
|
||||||
{item.name} ({item.id})
|
{item.name} ({item.id})
|
||||||
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
{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>
|
</Typography>
|
||||||
}
|
}
|
||||||
secondary={
|
secondary={
|
||||||
|
|||||||
@@ -758,6 +758,9 @@ export default function SSHServersPage() {
|
|||||||
<Typography component="div">
|
<Typography component="div">
|
||||||
{item.name} ({item.id})
|
{item.name} ({item.id})
|
||||||
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
{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>
|
</Typography>
|
||||||
}
|
}
|
||||||
secondary={
|
secondary={
|
||||||
@@ -765,9 +768,6 @@ export default function SSHServersPage() {
|
|||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
{item.host}:{item.port}
|
{item.host}:{item.port}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
|
||||||
Tags: {(item.tags || []).join(', ') || '-'}
|
|
||||||
</Typography>
|
|
||||||
{item.description? (
|
{item.description? (
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
{item.description}
|
{item.description}
|
||||||
|
|||||||
Reference in New Issue
Block a user