Skip to content

Commit c1a74fe

Browse files
committed
refactor AppsTab component
1 parent d08218f commit c1a74fe

File tree

2 files changed

+300
-8
lines changed

2 files changed

+300
-8
lines changed

client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { appsConfig } from "./config/apps.config";
3434
import { dataSourcesConfig } from "./config/data-sources.config";
3535
import { queryConfig } from "./config/query.config";
3636

37+
import AppsTab from "./components/AppsTab";
3738
const { Title, Text } = Typography;
3839
const { TabPane } = Tabs;
3940

@@ -160,14 +161,13 @@ const WorkspaceDetail: React.FC = () => {
160161

161162
{/* Tabs for Apps, Data Sources, and Queries */}
162163
<Tabs defaultActiveKey="apps">
163-
<TabPane tab={<span><AppstoreOutlined /> Apps</span>} key="apps">
164-
<DeployableItemsTab
165-
environment={environment}
166-
config={appsConfig}
167-
additionalParams={{ workspaceId: workspace.id }}
168-
title="Apps in this Workspace"
169-
/>
170-
</TabPane>
164+
// Replace the Apps TabPane in WorkspaceDetail.tsx with this:
165+
<TabPane tab={<span><AppstoreOutlined /> Apps</span>} key="apps">
166+
<AppsTab
167+
environment={environment}
168+
workspace={workspace}
169+
/>
170+
</TabPane>
171171

172172
<TabPane tab={<span><DatabaseOutlined /> Data Sources</span>} key="dataSources">
173173
<DeployableItemsTab
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Card, Button, Divider, Alert, message, Table, Tag, Input, Space, Tooltip } from 'antd';
3+
import { SyncOutlined, CloudUploadOutlined, SearchOutlined } from '@ant-design/icons';
4+
import Title from 'antd/lib/typography/Title';
5+
import { Environment } from '../types/environment.types';
6+
import { Workspace } from '../types/workspace.types';
7+
import { App, AppStats } from '../types/app.types';
8+
import { getMergedWorkspaceApps } from '../services/apps.service';
9+
import { Switch, Spin, Empty } from 'antd';
10+
import { ManagedObjectType, setManagedObject, unsetManagedObject } from '../services/managed-objects.service';
11+
import { useDeployModal } from '../context/DeployModalContext';
12+
import { appsConfig } from '../config/apps.config';
13+
14+
const { Search } = Input;
15+
16+
interface AppsTabProps {
17+
environment: Environment;
18+
workspace: Workspace;
19+
}
20+
21+
const AppsTab: React.FC<AppsTabProps> = ({ environment, workspace }) => {
22+
const [apps, setApps] = useState<App[]>([]);
23+
const [stats, setStats] = useState<AppStats>({
24+
total: 0,
25+
published: 0,
26+
managed: 0,
27+
unmanaged: 0
28+
});
29+
const [loading, setLoading] = useState(false);
30+
const [refreshing, setRefreshing] = useState(false);
31+
const [error, setError] = useState<string | null>(null);
32+
const [searchText, setSearchText] = useState('');
33+
const { openDeployModal } = useDeployModal();
34+
35+
// Fetch apps
36+
const fetchApps = async () => {
37+
if (!workspace.id || !environment) return;
38+
39+
setLoading(true);
40+
setError(null);
41+
42+
try {
43+
const result = await getMergedWorkspaceApps(
44+
workspace.id,
45+
environment.environmentId,
46+
environment.environmentApikey,
47+
environment.environmentApiServiceUrl!
48+
);
49+
50+
setApps(result.apps);
51+
52+
// Calculate stats
53+
const total = result.apps.length;
54+
const published = result.apps.filter(app => app.published).length;
55+
const managed = result.apps.filter(app => app.managed).length;
56+
57+
setStats({
58+
total,
59+
published,
60+
managed,
61+
unmanaged: total - managed
62+
});
63+
} catch (err) {
64+
setError(err instanceof Error ? err.message : "Failed to fetch apps");
65+
} finally {
66+
setLoading(false);
67+
setRefreshing(false);
68+
}
69+
};
70+
71+
useEffect(() => {
72+
fetchApps();
73+
}, [environment, workspace]);
74+
75+
// Handle refresh
76+
const handleRefresh = () => {
77+
setRefreshing(true);
78+
fetchApps();
79+
};
80+
81+
// Toggle managed status
82+
const handleToggleManaged = async (app: App, checked: boolean) => {
83+
setRefreshing(true);
84+
try {
85+
if (checked) {
86+
await setManagedObject(
87+
app.applicationGid,
88+
environment.environmentId,
89+
ManagedObjectType.APP,
90+
app.name
91+
);
92+
} else {
93+
await unsetManagedObject(
94+
app.applicationGid,
95+
environment.environmentId,
96+
ManagedObjectType.APP
97+
);
98+
}
99+
100+
// Update the app in state
101+
const updatedApps = apps.map(item => {
102+
if (item.applicationId === app.applicationId) {
103+
return { ...item, managed: checked };
104+
}
105+
return item;
106+
});
107+
108+
setApps(updatedApps);
109+
110+
// Update stats
111+
const managed = updatedApps.filter(app => app.managed).length;
112+
setStats(prev => ({
113+
...prev,
114+
managed,
115+
unmanaged: prev.total - managed
116+
}));
117+
118+
message.success(`${app.name} is now ${checked ? 'Managed' : 'Unmanaged'}`);
119+
return true;
120+
} catch (error) {
121+
message.error(`Failed to change managed status for ${app.name}`);
122+
return false;
123+
} finally {
124+
setRefreshing(false);
125+
}
126+
};
127+
128+
// Filter apps based on search
129+
const filteredApps = searchText
130+
? apps.filter(app =>
131+
app.name.toLowerCase().includes(searchText.toLowerCase()) ||
132+
app.applicationId.toLowerCase().includes(searchText.toLowerCase()))
133+
: apps;
134+
135+
// Table columns
136+
const columns = [
137+
{
138+
title: 'Name',
139+
dataIndex: 'name',
140+
key: 'name',
141+
render: (text: string) => <span className="app-name">{text}</span>
142+
},
143+
{
144+
title: 'ID',
145+
dataIndex: 'applicationId',
146+
key: 'applicationId',
147+
ellipsis: true,
148+
},
149+
{
150+
title: 'Published',
151+
dataIndex: 'published',
152+
key: 'published',
153+
render: (published: boolean) => (
154+
<Tag color={published ? 'green' : 'default'}>
155+
{published ? 'Published' : 'Draft'}
156+
</Tag>
157+
),
158+
},
159+
{
160+
title: 'Managed',
161+
key: 'managed',
162+
render: (_: any, app: App) => (
163+
<Switch
164+
checked={!!app.managed}
165+
onChange={(checked: boolean) => handleToggleManaged(app, checked)}
166+
loading={refreshing}
167+
size="small"
168+
/>
169+
),
170+
},
171+
{
172+
title: 'Actions',
173+
key: 'actions',
174+
render: (_: any, app: App) => (
175+
<Space onClick={(e) => e.stopPropagation()}>
176+
<Tooltip title={!app.managed ? "App must be managed before it can be deployed" : "Deploy this app to another environment"}>
177+
<Button
178+
type="primary"
179+
size="small"
180+
icon={<CloudUploadOutlined />}
181+
onClick={() => openDeployModal(app, appsConfig, environment)}
182+
disabled={!app.managed}
183+
>
184+
Deploy
185+
</Button>
186+
</Tooltip>
187+
</Space>
188+
),
189+
}
190+
];
191+
192+
return (
193+
<Card>
194+
{/* Header with refresh button */}
195+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "16px" }}>
196+
<Title level={5}>Apps in this Workspace</Title>
197+
<Button
198+
icon={<SyncOutlined spin={refreshing} />}
199+
onClick={handleRefresh}
200+
loading={loading}
201+
>
202+
Refresh
203+
</Button>
204+
</div>
205+
206+
{/* Stats display */}
207+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '24px', marginBottom: '16px' }}>
208+
<div>
209+
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>Total Apps</div>
210+
<div style={{ fontSize: '24px', fontWeight: 600 }}>{stats.total}</div>
211+
</div>
212+
<div>
213+
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>Published Apps</div>
214+
<div style={{ fontSize: '24px', fontWeight: 600 }}>{stats.published}</div>
215+
</div>
216+
<div>
217+
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>Managed Apps</div>
218+
<div style={{ fontSize: '24px', fontWeight: 600 }}>{stats.managed}</div>
219+
</div>
220+
<div>
221+
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>Unmanaged Apps</div>
222+
<div style={{ fontSize: '24px', fontWeight: 600 }}>{stats.unmanaged}</div>
223+
</div>
224+
</div>
225+
226+
<Divider style={{ margin: "16px 0" }} />
227+
228+
{/* Error display */}
229+
{error && (
230+
<Alert
231+
message="Error loading apps"
232+
description={error}
233+
type="error"
234+
showIcon
235+
style={{ marginBottom: "16px" }}
236+
/>
237+
)}
238+
239+
{/* Configuration warnings */}
240+
{(!environment.environmentApikey || !environment.environmentApiServiceUrl) && !error && (
241+
<Alert
242+
message="Configuration Issue"
243+
description="Missing required configuration: API key or API service URL"
244+
type="warning"
245+
showIcon
246+
style={{ marginBottom: "16px" }}
247+
/>
248+
)}
249+
250+
{/* Content */}
251+
{loading ? (
252+
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
253+
<Spin tip="Loading apps..." />
254+
</div>
255+
) : apps.length === 0 ? (
256+
<Empty
257+
description={error || "No apps found in this workspace"}
258+
image={Empty.PRESENTED_IMAGE_SIMPLE}
259+
/>
260+
) : (
261+
<>
262+
{/* Search Bar */}
263+
<div style={{ marginBottom: 16 }}>
264+
<Search
265+
placeholder="Search apps by name or ID"
266+
allowClear
267+
onSearch={value => setSearchText(value)}
268+
onChange={e => setSearchText(e.target.value)}
269+
style={{ width: 300 }}
270+
/>
271+
{searchText && filteredApps.length !== apps.length && (
272+
<div style={{ marginTop: 8 }}>
273+
Showing {filteredApps.length} of {apps.length} apps
274+
</div>
275+
)}
276+
</div>
277+
278+
<Table
279+
columns={columns}
280+
dataSource={filteredApps}
281+
rowKey="applicationId"
282+
pagination={{ pageSize: 10 }}
283+
size="middle"
284+
scroll={{ x: 'max-content' }}
285+
/>
286+
</>
287+
)}
288+
</Card>
289+
);
290+
};
291+
292+
export default AppsTab;

0 commit comments

Comments
 (0)