Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions frontend/src/pages/OpenClawSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ function fetchAuth<T>(url: string, options?: RequestInit): Promise<T> {
interface OpenClawSettingsProps {
agent: any;
agentId: string;
canManage: boolean;
}

export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsProps) {
export default function OpenClawSettings({ agent, agentId, canManage }: OpenClawSettingsProps) {
const { t, i18n } = useTranslation();
const queryClient = useQueryClient();
const navigate = useNavigate();
Expand All @@ -34,6 +35,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
const hasKey = agent?.has_api_key || false;

const handleRegenerate = async (autoCopy = false) => {
if (!canManage) return;
setRegenerating(true);
try {
const result = await fetchAuth<{ api_key: string }>(`/agents/${agentId}/api-key`, { method: 'POST' });
Expand All @@ -56,6 +58,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
};

const handleDelete = async () => {
if (!canManage) return;
setDeleting(true);
try {
await agentApi.delete(agentId);
Expand All @@ -75,6 +78,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
});

const handleScopeChange = async (newScope: string) => {
if (!canManage || !isOwner) return;
try {
await fetchAuth(`/agents/${agentId}/permissions`, {
method: 'PUT',
Expand All @@ -89,6 +93,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
};

const handleAccessLevelChange = async (newLevel: string) => {
if (!canManage || !isOwner) return;
try {
await fetchAuth(`/agents/${agentId}/permissions`, {
method: 'PUT',
Expand All @@ -103,6 +108,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
};

const isOwner = permData?.is_owner ?? false;
const canEditPermissions = canManage && isOwner;
const currentScope = permData?.scope_type === 'user' ? 'private' : (permData?.scope_type || 'company');
const currentAccessLevel = permData?.access_level || 'use';

Expand Down Expand Up @@ -146,13 +152,13 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
copiedLabel="Copied"
style={{ padding: '4px 12px', fontSize: '12px', whiteSpace: 'nowrap', minWidth: '70px', height: 'fit-content' }}
/>
<button
{canManage && <button
className="btn btn-secondary"
onClick={() => setShowConfirm(true)}
style={{ padding: '4px 12px', fontSize: '12px', whiteSpace: 'nowrap' }}
>
{isChinese ? '重新生成' : 'Regenerate'}
</button>
</button>}
</div>
);
}
Expand All @@ -169,15 +175,15 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
? (isChinese ? '旧版密钥(已加密隐藏),请重新生成以查看明文' : 'Legacy key (encrypted), please regenerate to view')
: (isChinese ? '未生成' : 'Not generated')}
</div>
<button
{canManage && <button
className="btn btn-secondary"
onClick={() => setShowConfirm(true)}
style={{ padding: '6px 16px', fontSize: '12px', whiteSpace: 'nowrap' }}
>
{isLegacyHash
? (isChinese ? '重新生成' : 'Regenerate')
: (isChinese ? '生成' : 'Generate')}
</button>
</button>}
</div>
);
})()}
Expand Down Expand Up @@ -214,7 +220,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
<button
className="btn btn-primary"
onClick={() => handleRegenerate(false)}
disabled={regenerating}
disabled={!canManage || regenerating}
style={{ padding: '5px 14px', fontSize: '12px' }}
>
{regenerating
Expand Down Expand Up @@ -243,22 +249,22 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
style={{
display: 'flex', alignItems: 'center', gap: '10px',
padding: '12px 14px', borderRadius: '8px',
cursor: isOwner ? 'pointer' : 'default',
cursor: canEditPermissions ? 'pointer' : 'default',
border: currentScope === scope
? '1px solid var(--accent-primary)'
: '1px solid var(--border-subtle)',
background: currentScope === scope
? 'rgba(99,102,241,0.06)'
: 'transparent',
opacity: isOwner ? 1 : 0.7,
opacity: canEditPermissions ? 1 : 0.7,
transition: 'all 0.15s',
}}
>
<input
type="radio"
name="perm_scope_oc"
checked={currentScope === scope}
disabled={!isOwner}
disabled={!canEditPermissions}
onChange={() => handleScopeChange(scope)}
style={{ accentColor: 'var(--accent-primary)' }}
/>
Expand All @@ -281,7 +287,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
</div>

{/* Access Level for company scope */}
{currentScope === 'company' && isOwner && (
{currentScope === 'company' && canEditPermissions && (
<div style={{ borderTop: '1px solid var(--border-subtle)', paddingTop: '12px' }}>
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '8px' }}>
{t('agent.settings.perm.defaultAccess', 'Default Access Level')}
Expand Down Expand Up @@ -332,7 +338,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
</div>

{/* ── Danger Zone: Delete Agent ── */}
{isOwner && (
{canEditPermissions && (
<div className="card" style={{
marginBottom: '12px',
border: '1px solid rgba(255,80,80,0.2)',
Expand Down
18 changes: 10 additions & 8 deletions frontend/src/pages/agent-detail/AgentDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4885,11 +4885,12 @@ export default function AgentDetailPage() {
</>
{(agent as any)?.agent_type !== 'openclaw' && (
<>
{agent.status === 'stopped' ? (
{canManage && agent.status === 'stopped' && (
<button className="btn btn-secondary" onClick={async () => { await agentApi.start(id!); queryClient.invalidateQueries({ queryKey: ['agent', id] }); }}>{t('agent.actions.start')}</button>
) : agent.status === 'running' ? (
)}
{canManage && agent.status === 'running' && (
<button className="btn btn-secondary" onClick={async () => { await agentApi.stop(id!); queryClient.invalidateQueries({ queryKey: ['agent', id] }); }}>{t('agent.actions.stop')}</button>
) : null}
)}
</>
)}
</div>
Expand All @@ -4900,7 +4901,7 @@ export default function AgentDetailPage() {
{activeTab !== 'chat' && <div className="tabs">
{AGENT_DETAIL_TABS.filter(tab => {
if (['aware', 'workspace', 'chat'].includes(tab)) return false;
// 'use' access: hide settings and approvals tabs
// 'use' access keeps the existing tab bar unchanged; settings remains available via its own entry.
if ((agent as any)?.access_level === 'use') {
if (tab === 'settings' || tab === 'approvals') return false;
}
Expand Down Expand Up @@ -5109,7 +5110,7 @@ export default function AgentDetailPage() {
{/* Quick Actions */}
<div style={{ display: 'flex', gap: '10px', marginTop: '20px' }}>
<button className="btn btn-secondary" onClick={() => setActiveTab('chat')}>{t('agent.actions.chat')}</button>
<button className="btn btn-secondary" onClick={() => setActiveTab('settings')}>{t('agent.tabs.settings')}</button>
{canManage && <button className="btn btn-secondary" onClick={() => setActiveTab('settings')}>{t('agent.tabs.settings')}</button>}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the read-only settings tab reachable

For use access, the tab filter above still removes settings from the tab bar, and the top settings button only navigates to /settings where useAgentDetailRoute defaults to the status tab. With this new canManage guard, use-only users lose the only visible control that selected the actual settings tab, so the read-only SettingsTab/OpenClawSettings work added in this change is not discoverable unless the user manually edits the hash to #settings.

Useful? React with 👍 / 👎.

</div>
</div>
);
Expand Down Expand Up @@ -5353,9 +5354,10 @@ export default function AgentDetailPage() {
{trig.is_enabled ? t('agent.aware.inProgress') : t('agent.aware.completed')}
</span>
<div style={{ display: 'flex', gap: '4px' }}>
{!trig.is_system && <button className="btn btn-ghost" style={{ padding: '2px 6px', fontSize: '11px', color: 'var(--error)' }}
{canManage && !trig.is_system && <button className="btn btn-ghost" style={{ padding: '2px 6px', fontSize: '11px', color: 'var(--error)' }}
onClick={async (e) => {
e.stopPropagation();
if (!canManage) return;
const ok = await dialog.confirm(t('agent.aware.deleteTriggerConfirm', { name: trig.name }), { title: '删除触发器', danger: true, confirmLabel: '删除' });
if (ok) {
await triggerApi.delete(id!, trig.id);
Expand Down Expand Up @@ -5782,7 +5784,7 @@ export default function AgentDetailPage() {
upload: (file, path, onProgress) => fileApi.upload(id!, file, path + '/', onProgress),
downloadUrl: (p) => fileApi.downloadUrl(id!, p),
};
return <FileBrowser api={adapter} rootPath="workspace" features={{ upload: true, newFile: true, newFolder: true, edit: true, delete: canManage, directoryNavigation: true }} />;
return <FileBrowser api={adapter} rootPath="workspace" features={{ upload: canManage, newFile: canManage, newFolder: canManage, edit: canManage, delete: canManage, directoryNavigation: true }} />;
})()
}

Expand Down Expand Up @@ -6716,7 +6718,7 @@ export default function AgentDetailPage() {

{/* ── Approvals Tab ── */}
{
activeTab === 'approvals' && id && <ApprovalsTab agentId={id} />
activeTab === 'approvals' && id && <ApprovalsTab agentId={id} canManage={canManage} />
}

{/* ── Settings Tab ── */}
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/pages/agent-detail/tabs/ApprovalsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';

import { fetchAuth } from '../utils/fetchAuth';

export default function ApprovalsTab({ agentId }: { agentId: string }) {
export default function ApprovalsTab({ agentId, canManage }: { agentId: string; canManage: boolean }) {
const { i18n } = useTranslation();
const queryClient = useQueryClient();
const isChinese = i18n.language?.startsWith('zh');
Expand All @@ -16,6 +16,7 @@ export default function ApprovalsTab({ agentId }: { agentId: string }) {

const resolveMut = useMutation({
mutationFn: async ({ approvalId, action }: { approvalId: string; action: string }) => {
if (!canManage) return;
const token = localStorage.getItem('token');
return fetch(`/api/agents/${agentId}/approvals/${approvalId}/resolve`, {
method: 'POST',
Expand Down Expand Up @@ -79,24 +80,28 @@ export default function ApprovalsTab({ agentId }: { agentId: string }) {
{typeof approval.details === 'string' ? approval.details : JSON.stringify(approval.details, null, 2)}
</div>
)}
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
{canManage && <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<button
className="btn btn-primary"
style={{ padding: '6px 16px', fontSize: '12px' }}
onClick={() => resolveMut.mutate({ approvalId: approval.id, action: 'approve' })}
disabled={resolveMut.isPending}
onClick={() => {
if (canManage) resolveMut.mutate({ approvalId: approval.id, action: 'approve' });
}}
disabled={!canManage || resolveMut.isPending}
>
{isChinese ? '批准' : 'Approve'}
</button>
<button
className="btn btn-danger"
style={{ padding: '6px 16px', fontSize: '12px' }}
onClick={() => resolveMut.mutate({ approvalId: approval.id, action: 'reject' })}
disabled={resolveMut.isPending}
onClick={() => {
if (canManage) resolveMut.mutate({ approvalId: approval.id, action: 'reject' });
}}
disabled={!canManage || resolveMut.isPending}
>
{isChinese ? '拒绝' : 'Reject'}
</button>
</div>
</div>}
</div>
))}
<div style={{ borderTop: '1px solid var(--border-subtle)', margin: '16px 0' }} />
Expand Down
Loading