Skip to content

Revamp the Environments UI and Refactor #1687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0f5440d
Add managed/unmanged filtering on column
iamfaran May 9, 2025
8e0f94b
Add search filter for the objects
iamfaran May 9, 2025
9883336
Improve the Environment Not Found UI
iamfaran May 9, 2025
b6d74d2
create a util function and add tags in Deployment Modal
iamfaran May 12, 2025
bb81f1f
fix endpoint for DS and ql
iamfaran May 14, 2025
b5647a7
updated managed endpoints
iamfaran May 15, 2025
d08218f
fix switch for objects
iamfaran May 15, 2025
c1a74fe
refactor AppsTab component
iamfaran May 15, 2025
0a1879b
Seperate DS tab
iamfaran May 15, 2025
dffc683
Seperate Queries Tab
iamfaran May 15, 2025
7410765
Add seperate workspace and usergroups tab
iamfaran May 15, 2025
6383d7f
remove unnecessary code
iamfaran May 15, 2025
199c869
add audit buttons in tabs
iamfaran May 15, 2025
f4fba8e
Update Apps UI
iamfaran May 15, 2025
c3a770e
update UI for DS and queries
iamfaran May 15, 2025
3e35b5d
update UI for workspaces tab
iamfaran May 16, 2025
548e73b
update UI user groups tab
iamfaran May 16, 2025
7f4d11f
update environment detail header
iamfaran May 16, 2025
9a8329e
create utils for different env colors
iamfaran May 16, 2025
b3799a0
update environments listing page
iamfaran May 16, 2025
933878e
add breadcrumbs component and fix tabs styling
iamfaran May 16, 2025
8880f3e
fix environments icon on listing page
iamfaran May 16, 2025
b97f125
fix refresh buttons
iamfaran May 16, 2025
f3a36d8
fix tabs rendering issue
iamfaran May 16, 2025
4b9f37d
fix refresh button for the environment listing
iamfaran May 16, 2025
2b4a718
setup for new managed-obj endpoint
iamfaran May 16, 2025
7660432
implement new managed obj endpoints
iamfaran May 16, 2025
6fcafe4
update workspace header UI
iamfaran May 17, 2025
009d6fa
fix width issue for Workspace detai page
iamfaran May 19, 2025
cec515c
add managed filter
iamfaran May 19, 2025
cc3e96a
update UI for workspace header banner
iamfaran May 19, 2025
7a311e6
fix search UI for datasources
iamfaran May 19, 2025
66365b3
add managed filter for Workspaces Tab
iamfaran May 19, 2025
359d94d
change action button positions for TABS
iamfaran May 19, 2025
cde8c0c
update breadcrumbs position
iamfaran May 19, 2025
c1c874b
test managed linked object
iamfaran May 19, 2025
23f526d
make a util function to link the managed object
iamfaran May 19, 2025
266def6
Merge branch 'ee-setup' of github.com:lowcoder-org/lowcoder into feat…
iamfaran May 19, 2025
c1d68dc
Merge branch 'ui/environments' into feat/environments
iamfaran May 19, 2025
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
Prev Previous commit
Next Next commit
Seperate DS tab
  • Loading branch information
iamfaran committed May 15, 2025
commit 0a1879be9a70e203d5decdc34c042af9c0421022
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { dataSourcesConfig } from "./config/data-sources.config";
import { queryConfig } from "./config/query.config";

import AppsTab from "./components/AppsTab";
import DataSourcesTab from "./components/DataSourcesTab";
const { Title, Text } = Typography;
const { TabPane } = Tabs;

Expand Down Expand Up @@ -170,11 +171,9 @@ const WorkspaceDetail: React.FC = () => {
</TabPane>

<TabPane tab={<span><DatabaseOutlined /> Data Sources</span>} key="dataSources">
<DeployableItemsTab
<DataSourcesTab
environment={environment}
config={dataSourcesConfig}
additionalParams={{ workspaceId: workspace.id }}
title="Data Sources in this Workspace"
workspace={workspace}
/>
</TabPane>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import React, { useState, useEffect } from 'react';
import { Card, Button, Divider, Alert, message, Table, Tag, Input, Space, Tooltip } from 'antd';
import { SyncOutlined, CloudUploadOutlined, DatabaseOutlined } from '@ant-design/icons';
import Title from 'antd/lib/typography/Title';
import { Environment } from '../types/environment.types';
import { Workspace } from '../types/workspace.types';
import { DataSource } from '../types/datasource.types';
import { getMergedWorkspaceDataSources } from '../services/datasources.service';
import { Switch, Spin, Empty } from 'antd';
import { ManagedObjectType, setManagedObject, unsetManagedObject } from '../services/managed-objects.service';
import { useDeployModal } from '../context/DeployModalContext';
import { dataSourcesConfig } from '../config/data-sources.config';

const { Search } = Input;

interface DataSourcesTabProps {
environment: Environment;
workspace: Workspace;
}

const DataSourcesTab: React.FC<DataSourcesTabProps> = ({ environment, workspace }) => {
const [dataSources, setDataSources] = useState<DataSource[]>([]);
const [stats, setStats] = useState({
total: 0,
types: 0,
managed: 0,
unmanaged: 0
});
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchText, setSearchText] = useState('');
const { openDeployModal } = useDeployModal();

// Fetch data sources
const fetchDataSources = async () => {
if (!workspace.id || !environment) return;

setLoading(true);
setError(null);

try {
const result = await getMergedWorkspaceDataSources(
workspace.id,
environment.environmentId,
environment.environmentApikey,
environment.environmentApiServiceUrl!
);

setDataSources(result.dataSources);
setStats(result.stats);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to fetch data sources");
} finally {
setLoading(false);
setRefreshing(false);
}
};

useEffect(() => {
fetchDataSources();
}, [environment, workspace]);

// Handle refresh
const handleRefresh = () => {
setRefreshing(true);
fetchDataSources();
};

// Toggle managed status
const handleToggleManaged = async (dataSource: DataSource, checked: boolean) => {
setRefreshing(true);
try {
if (checked) {
await setManagedObject(
dataSource.gid,
environment.environmentId,
ManagedObjectType.DATASOURCE,
dataSource.name
);
} else {
await unsetManagedObject(
dataSource.gid,
environment.environmentId,
ManagedObjectType.DATASOURCE
);
}

// Update the data source in state
const updatedDataSources = dataSources.map(item => {
if (item.id === dataSource.id) {
return { ...item, managed: checked };
}
return item;
});

setDataSources(updatedDataSources);

// Update stats
const managed = updatedDataSources.filter(ds => ds.managed).length;
setStats(prev => ({
...prev,
managed,
unmanaged: prev.total - managed
}));

message.success(`${dataSource.name} is now ${checked ? 'Managed' : 'Unmanaged'}`);
return true;
} catch (error) {
message.error(`Failed to change managed status for ${dataSource.name}`);
return false;
} finally {
setRefreshing(false);
}
};

// Filter data sources based on search
const filteredDataSources = searchText
? dataSources.filter(ds =>
ds.name.toLowerCase().includes(searchText.toLowerCase()) ||
ds.id.toString().toLowerCase().includes(searchText.toLowerCase()))
: dataSources;

// Table columns
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (text: string) => <span className="datasource-name">{text}</span>
},
{
title: 'ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
},
{
title: 'Type',
dataIndex: 'type',
key: 'type',
render: (type: string) => (
<Tag color="blue">{type}</Tag>
),
},
{
title: 'Managed',
key: 'managed',
render: (_: any, dataSource: DataSource) => (
<Switch
checked={!!dataSource.managed}
onChange={(checked: boolean) => handleToggleManaged(dataSource, checked)}
loading={refreshing}
size="small"
/>
),
},
{
title: 'Actions',
key: 'actions',
render: (_: any, dataSource: DataSource) => (
<Space onClick={(e) => e.stopPropagation()}>
<Tooltip title={!dataSource.managed ? "Data source must be managed before it can be deployed" : "Deploy this data source to another environment"}>
<Button
type="primary"
size="small"
icon={<CloudUploadOutlined />}
onClick={() => openDeployModal(dataSource, dataSourcesConfig, environment)}
disabled={!dataSource.managed}
>
Deploy
</Button>
</Tooltip>
</Space>
),
}
];

return (
<Card>
{/* Header with refresh button */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "16px" }}>
<Title level={5}>Data Sources in this Workspace</Title>
<Button
icon={<SyncOutlined spin={refreshing} />}
onClick={handleRefresh}
loading={loading}
>
Refresh
</Button>
</div>

{/* Stats display */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '24px', marginBottom: '16px' }}>
<div>
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>Total Data Sources</div>
<div style={{ fontSize: '24px', fontWeight: 600 }}>{stats.total}</div>
</div>
<div>
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>Types</div>
<div style={{ fontSize: '24px', fontWeight: 600 }}>{stats.types}</div>
</div>
<div>
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>Managed</div>
<div style={{ fontSize: '24px', fontWeight: 600 }}>{stats.managed}</div>
</div>
<div>
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>Unmanaged</div>
<div style={{ fontSize: '24px', fontWeight: 600 }}>{stats.unmanaged}</div>
</div>
</div>

<Divider style={{ margin: "16px 0" }} />

{/* Error display */}
{error && (
<Alert
message="Error loading data sources"
description={error}
type="error"
showIcon
style={{ marginBottom: "16px" }}
/>
)}

{/* Configuration warnings */}
{(!environment.environmentApikey || !environment.environmentApiServiceUrl) && !error && (
<Alert
message="Configuration Issue"
description="Missing required configuration: API key or API service URL"
type="warning"
showIcon
style={{ marginBottom: "16px" }}
/>
)}

{/* Content */}
{loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
<Spin tip="Loading data sources..." />
</div>
) : dataSources.length === 0 ? (
<Empty
description={error || "No data sources found in this workspace"}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
) : (
<>
{/* Search Bar */}
<div style={{ marginBottom: 16 }}>
<Search
placeholder="Search data sources by name or ID"
allowClear
onSearch={value => setSearchText(value)}
onChange={e => setSearchText(e.target.value)}
style={{ width: 300 }}
/>
{searchText && filteredDataSources.length !== dataSources.length && (
<div style={{ marginTop: 8 }}>
Showing {filteredDataSources.length} of {dataSources.length} data sources
</div>
)}
</div>

<Table
columns={columns}
dataSource={filteredDataSources}
rowKey="id"
pagination={{ pageSize: 10 }}
size="middle"
scroll={{ x: 'max-content' }}
/>
</>
)}
</Card>
);
};

export default DataSourcesTab;