Skip to content

Commit 5d27123

Browse files
FEATURE (adaptivity): Add mobile adaptivity
1 parent 79ca374 commit 5d27123

15 files changed

+792
-456
lines changed

frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ export function EditNotifierComponent({
208208
return (
209209
<div>
210210
{isShowName && (
211-
<div className="mb-1 flex items-center">
212-
<div className="min-w-[130px]">Name</div>
211+
<div className="mb-1 flex w-full flex-col items-start sm:flex-row sm:items-center">
212+
<div className="mb-1 min-w-[150px] sm:mb-0">Name</div>
213213

214214
<Input
215215
value={notifier?.name || ''}
@@ -224,28 +224,30 @@ export function EditNotifierComponent({
224224
</div>
225225
)}
226226

227-
<div className="mb-1 flex items-center">
228-
<div className="w-[150px] min-w-[150px]">Type</div>
229-
230-
<Select
231-
value={notifier?.notifierType}
232-
options={[
233-
{ label: 'Telegram', value: NotifierType.TELEGRAM },
234-
{ label: 'Email', value: NotifierType.EMAIL },
235-
{ label: 'Webhook', value: NotifierType.WEBHOOK },
236-
{ label: 'Slack', value: NotifierType.SLACK },
237-
{ label: 'Discord', value: NotifierType.DISCORD },
238-
{ label: 'Teams', value: NotifierType.TEAMS },
239-
]}
240-
onChange={(value) => {
241-
setNotifierType(value);
242-
setIsUnsaved(true);
243-
}}
244-
size="small"
245-
className="w-full max-w-[250px]"
246-
/>
247-
248-
<img src={getNotifierLogoFromType(notifier?.notifierType)} className="ml-2 h-4 w-4" />
227+
<div className="mb-1 flex w-full flex-col items-start sm:flex-row sm:items-center">
228+
<div className="mb-1 min-w-[150px] sm:mb-0">Type</div>
229+
230+
<div className="flex items-center">
231+
<Select
232+
value={notifier?.notifierType}
233+
options={[
234+
{ label: 'Telegram', value: NotifierType.TELEGRAM },
235+
{ label: 'Email', value: NotifierType.EMAIL },
236+
{ label: 'Webhook', value: NotifierType.WEBHOOK },
237+
{ label: 'Slack', value: NotifierType.SLACK },
238+
{ label: 'Discord', value: NotifierType.DISCORD },
239+
{ label: 'Teams', value: NotifierType.TEAMS },
240+
]}
241+
onChange={(value) => {
242+
setNotifierType(value);
243+
setIsUnsaved(true);
244+
}}
245+
size="small"
246+
className="w-[250px] max-w-[250px]"
247+
/>
248+
249+
<img src={getNotifierLogoFromType(notifier?.notifierType)} className="ml-2 h-4 w-4" />
250+
</div>
249251
</div>
250252

251253
<div className="mt-5" />

frontend/src/features/settings/ui/AuditLogsComponent.tsx

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
77
import { auditLogApi } from '../../../entity/audit-logs/api/auditLogApi';
88
import type { AuditLog } from '../../../entity/audit-logs/model/AuditLog';
99
import type { GetAuditLogsRequest } from '../../../entity/audit-logs/model/GetAuditLogsRequest';
10+
import { useIsMobile } from '../../../shared/hooks';
1011
import { getUserTimeFormat } from '../../../shared/time';
1112

1213
interface Props {
@@ -15,6 +16,7 @@ interface Props {
1516

1617
export function AuditLogsComponent({ scrollContainerRef: externalScrollRef }: Props) {
1718
const { message } = App.useApp();
19+
const isMobile = useIsMobile();
1820
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
1921
const [isLoading, setIsLoading] = useState(true);
2022
const [isLoadingMore, setIsLoadingMore] = useState(false);
@@ -158,6 +160,49 @@ export function AuditLogsComponent({ scrollContainerRef: externalScrollRef }: Pr
158160
},
159161
];
160162

163+
const renderAuditLogCard = (log: AuditLog) => {
164+
const date = dayjs(log.createdAt);
165+
const timeFormat = getUserTimeFormat();
166+
167+
const getUserDisplay = () => {
168+
if (!log.userEmail && !log.userName) {
169+
return (
170+
<span className="inline-block rounded-full bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-600">
171+
System
172+
</span>
173+
);
174+
}
175+
176+
const displayText = log.userName ? `${log.userName} (${log.userEmail})` : log.userEmail;
177+
178+
return (
179+
<span className="inline-block rounded-full bg-blue-100 px-1.5 py-0.5 text-xs font-medium text-blue-800">
180+
{displayText}
181+
</span>
182+
);
183+
};
184+
185+
return (
186+
<div key={log.id} className="mb-3 rounded-lg border border-gray-200 bg-white p-3 shadow-sm">
187+
<div className="flex items-start justify-between">
188+
<div className="flex-1">{getUserDisplay()}</div>
189+
<div className="text-right text-xs text-gray-500">
190+
<div>{date.format(timeFormat.format)}</div>
191+
<div className="text-gray-400">{date.fromNow()}</div>
192+
</div>
193+
</div>
194+
<div className="mt-2 text-sm text-gray-900">{log.message}</div>
195+
{log.workspaceName && (
196+
<div className="mt-2">
197+
<span className="inline-block rounded-full bg-blue-100 px-1.5 py-0.5 text-xs font-medium text-blue-800">
198+
{log.workspaceName}
199+
</span>
200+
</div>
201+
)}
202+
</div>
203+
);
204+
};
205+
161206
return (
162207
<div className="max-w-[1200px]">
163208
<div className="mb-4 flex items-center justify-between">
@@ -175,16 +220,24 @@ export function AuditLogsComponent({ scrollContainerRef: externalScrollRef }: Pr
175220
<div className="flex h-64 items-center justify-center">
176221
<Spin indicator={<LoadingOutlined spin />} size="large" />
177222
</div>
223+
) : auditLogs.length === 0 ? (
224+
<div className="flex h-32 items-center justify-center text-gray-500">
225+
No audit logs found.
226+
</div>
178227
) : (
179228
<>
180-
<Table
181-
columns={columns}
182-
dataSource={auditLogs}
183-
pagination={false}
184-
rowKey="id"
185-
size="small"
186-
className="mb-4"
187-
/>
229+
{isMobile ? (
230+
<div>{auditLogs.map(renderAuditLogCard)}</div>
231+
) : (
232+
<Table
233+
columns={columns}
234+
dataSource={auditLogs}
235+
pagination={false}
236+
rowKey="id"
237+
size="small"
238+
className="mb-4"
239+
/>
240+
)}
188241

189242
{isLoadingMore && (
190243
<div className="flex justify-center py-4">
@@ -195,7 +248,7 @@ export function AuditLogsComponent({ scrollContainerRef: externalScrollRef }: Pr
195248

196249
{!hasMore && auditLogs.length > 0 && (
197250
<div className="py-4 text-center text-sm text-gray-500">
198-
All logs loaded ({total} total)
251+
All logs loaded ({auditLogs.length} total)
199252
</div>
200253
)}
201254
</>

frontend/src/features/settings/ui/SettingsComponent.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,14 @@ export function SettingsComponent({ contentHeight }: Props) {
9191
console.log(`isCloud = ${IS_CLOUD}`);
9292

9393
return (
94-
<div className="flex grow pl-3">
94+
<div className="flex grow sm:pl-5">
9595
<div className="w-full">
9696
<div
9797
ref={scrollContainerRef}
9898
className="grow overflow-y-auto rounded bg-white p-5 shadow"
9999
style={{ height: contentHeight }}
100100
>
101-
<h1 className="text-2xl font-bold">Postgresus Settings</h1>
101+
<h1 className="text-2xl font-bold">Postgresus settings</h1>
102102

103103
<div className="mt-6">
104104
{isLoading ? (
@@ -228,7 +228,7 @@ export function SettingsComponent({ contentHeight }: Props) {
228228
<div className="group relative">
229229
<div className="flex items-center rounded-md border border-gray-300 bg-gray-50 px-3 py-2 !font-mono text-sm text-gray-700">
230230
<code
231-
className="flex-1 cursor-pointer transition-colors select-all hover:text-blue-600"
231+
className="flex-1 cursor-pointer break-all transition-colors select-all hover:text-blue-600"
232232
onClick={() => {
233233
window.open(`${getApplicationServer()}/api/v1/system/health`, '_blank');
234234
}}

frontend/src/features/storages/ui/StoragesComponent.tsx

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
44
import { storageApi } from '../../../entity/storages';
55
import type { Storage } from '../../../entity/storages';
66
import type { WorkspaceResponse } from '../../../entity/workspaces';
7+
import { useIsMobile } from '../../../shared/hooks';
78
import { StorageCardComponent } from './StorageCardComponent';
89
import { StorageComponent } from './StorageComponent';
910
import { EditStorageComponent } from './edit/EditStorageComponent';
@@ -14,22 +15,42 @@ interface Props {
1415
isCanManageStorages: boolean;
1516
}
1617

18+
const SELECTED_STORAGE_STORAGE_KEY = 'selectedStorageId';
19+
1720
export const StoragesComponent = ({ contentHeight, workspace, isCanManageStorages }: Props) => {
21+
const isMobile = useIsMobile();
1822
const [isLoading, setIsLoading] = useState(true);
1923
const [storages, setStorages] = useState<Storage[]>([]);
2024

2125
const [isShowAddStorage, setIsShowAddStorage] = useState(false);
2226
const [selectedStorageId, setSelectedStorageId] = useState<string | undefined>(undefined);
2327

28+
const updateSelectedStorageId = (storageId: string | undefined) => {
29+
setSelectedStorageId(storageId);
30+
if (storageId) {
31+
localStorage.setItem(`${SELECTED_STORAGE_STORAGE_KEY}_${workspace.id}`, storageId);
32+
} else {
33+
localStorage.removeItem(`${SELECTED_STORAGE_STORAGE_KEY}_${workspace.id}`);
34+
}
35+
};
36+
2437
const loadStorages = () => {
2538
setIsLoading(true);
2639

2740
storageApi
2841
.getStorages(workspace.id)
2942
.then((storages: Storage[]) => {
3043
setStorages(storages);
31-
if (!selectedStorageId) {
32-
setSelectedStorageId(storages[0]?.id);
44+
if (!selectedStorageId && !isMobile) {
45+
// On desktop, auto-select a storage; on mobile, keep it unselected
46+
const savedStorageId = localStorage.getItem(
47+
`${SELECTED_STORAGE_STORAGE_KEY}_${workspace.id}`,
48+
);
49+
const storageToSelect =
50+
savedStorageId && storages.some((s) => s.id === savedStorageId)
51+
? savedStorageId
52+
: storages[0]?.id;
53+
updateSelectedStorageId(storageToSelect);
3354
}
3455
})
3556
.catch((e: Error) => alert(e.message))
@@ -54,45 +75,66 @@ export const StoragesComponent = ({ contentHeight, workspace, isCanManageStorage
5475
</Button>
5576
);
5677

78+
// On mobile, show either the list or the storage details
79+
const showStorageList = !isMobile || !selectedStorageId;
80+
const showStorageDetails = selectedStorageId && (!isMobile || selectedStorageId);
81+
5782
return (
5883
<>
5984
<div className="flex grow">
60-
<div
61-
className="mx-3 w-[250px] min-w-[250px] overflow-y-auto"
62-
style={{ height: contentHeight }}
63-
>
64-
{storages.length >= 5 && isCanManageStorages && addStorageButton}
65-
66-
{storages.map((storage) => (
67-
<StorageCardComponent
68-
key={storage.id}
69-
storage={storage}
70-
selectedStorageId={selectedStorageId}
71-
setSelectedStorageId={setSelectedStorageId}
72-
/>
73-
))}
74-
75-
{storages.length < 5 && isCanManageStorages && addStorageButton}
76-
77-
<div className="mx-3 text-center text-xs text-gray-500">
78-
Storage - is a place where backups will be stored (local disk, S3, etc.)
85+
{showStorageList && (
86+
<div
87+
className="w-full overflow-y-auto md:mx-3 md:w-[250px] md:min-w-[250px] md:pr-2"
88+
style={{ height: contentHeight }}
89+
>
90+
{storages.length >= 5 && isCanManageStorages && addStorageButton}
91+
92+
{storages.map((storage) => (
93+
<StorageCardComponent
94+
key={storage.id}
95+
storage={storage}
96+
selectedStorageId={selectedStorageId}
97+
setSelectedStorageId={updateSelectedStorageId}
98+
/>
99+
))}
100+
101+
{storages.length < 5 && isCanManageStorages && addStorageButton}
102+
103+
<div className="mx-3 text-center text-xs text-gray-500">
104+
Storage - is a place where backups will be stored (local disk, S3, etc.)
105+
</div>
79106
</div>
80-
</div>
107+
)}
81108

82-
{selectedStorageId && (
83-
<StorageComponent
84-
storageId={selectedStorageId}
85-
onStorageChanged={() => {
86-
loadStorages();
87-
}}
88-
onStorageDeleted={() => {
89-
loadStorages();
90-
setSelectedStorageId(
91-
storages.filter((storage) => storage.id !== selectedStorageId)[0]?.id,
92-
);
93-
}}
94-
isCanManageStorages={isCanManageStorages}
95-
/>
109+
{showStorageDetails && (
110+
<div className="flex w-full flex-col md:flex-1">
111+
{isMobile && (
112+
<div className="mb-2">
113+
<Button
114+
type="default"
115+
onClick={() => updateSelectedStorageId(undefined)}
116+
className="w-full"
117+
>
118+
← Back to storages
119+
</Button>
120+
</div>
121+
)}
122+
123+
<StorageComponent
124+
storageId={selectedStorageId}
125+
onStorageChanged={() => {
126+
loadStorages();
127+
}}
128+
onStorageDeleted={() => {
129+
const remainingStorages = storages.filter(
130+
(storage) => storage.id !== selectedStorageId,
131+
);
132+
updateSelectedStorageId(remainingStorages[0]?.id);
133+
loadStorages();
134+
}}
135+
isCanManageStorages={isCanManageStorages}
136+
/>
137+
</div>
96138
)}
97139
</div>
98140

0 commit comments

Comments
 (0)