Skip to content

Commit c848f81

Browse files
gnugomeznetomi
andauthored
feat: adding admin logs to the backoffice (eclipse-openvsx#1582)
* feat: adding paginated admin logs endpoint * feat: adding admin logs ui * formatting * minor style update --------- Co-authored-by: Thomas Neidhart <thomas.neidhart@eclipse-foundation.org>
1 parent 4bf4aa8 commit c848f81

15 files changed

Lines changed: 375 additions & 23 deletions

File tree

server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,51 @@
99
********************************************************************************/
1010
package org.eclipse.openvsx.admin;
1111

12-
import io.swagger.v3.oas.annotations.Operation;
13-
import io.swagger.v3.oas.annotations.Parameter;
14-
import io.swagger.v3.oas.annotations.media.Content;
15-
import io.swagger.v3.oas.annotations.media.Schema;
16-
import io.swagger.v3.oas.annotations.responses.ApiResponse;
12+
import java.net.URI;
13+
import java.time.Period;
14+
import java.time.format.DateTimeParseException;
15+
import java.util.Collections;
16+
import java.util.List;
17+
import java.util.stream.Collectors;
18+
1719
import org.apache.commons.lang3.StringUtils;
1820
import org.eclipse.openvsx.LocalRegistryService;
1921
import org.eclipse.openvsx.entities.AdminStatistics;
2022
import org.eclipse.openvsx.entities.NamespaceMembership;
2123
import org.eclipse.openvsx.entities.PersistedLog;
22-
import org.eclipse.openvsx.json.*;
24+
import org.eclipse.openvsx.json.AdminStatisticsJson;
25+
import org.eclipse.openvsx.json.ChangeNamespaceJson;
26+
import org.eclipse.openvsx.json.ExtensionJson;
27+
import org.eclipse.openvsx.json.NamespaceJson;
28+
import org.eclipse.openvsx.json.NamespaceMembershipListJson;
29+
import org.eclipse.openvsx.json.PersistedLogJson;
30+
import org.eclipse.openvsx.json.ResultJson;
31+
import org.eclipse.openvsx.json.StatsJson;
32+
import org.eclipse.openvsx.json.TargetPlatformVersionJson;
33+
import org.eclipse.openvsx.json.UserPublishInfoJson;
2334
import org.eclipse.openvsx.repositories.RepositoryService;
2435
import org.eclipse.openvsx.search.SearchUtilService;
2536
import org.eclipse.openvsx.util.*;
37+
import org.springframework.data.domain.Page;
38+
import org.springframework.data.domain.Pageable;
2639
import org.springframework.data.util.Streamable;
2740
import org.springframework.http.HttpStatus;
2841
import org.springframework.http.MediaType;
2942
import org.springframework.http.ResponseEntity;
30-
import org.springframework.web.bind.annotation.*;
43+
import org.springframework.web.bind.annotation.CrossOrigin;
44+
import org.springframework.web.bind.annotation.GetMapping;
45+
import org.springframework.web.bind.annotation.PathVariable;
46+
import org.springframework.web.bind.annotation.PostMapping;
47+
import org.springframework.web.bind.annotation.RequestBody;
48+
import org.springframework.web.bind.annotation.RequestParam;
49+
import org.springframework.web.bind.annotation.RestController;
3150
import org.springframework.web.server.ResponseStatusException;
3251

33-
import java.net.URI;
34-
import java.time.Period;
35-
import java.time.format.DateTimeParseException;
36-
import java.util.Collections;
37-
import java.util.List;
38-
import java.util.stream.Collectors;
52+
import io.swagger.v3.oas.annotations.Operation;
53+
import io.swagger.v3.oas.annotations.Parameter;
54+
import io.swagger.v3.oas.annotations.media.Content;
55+
import io.swagger.v3.oas.annotations.media.Schema;
56+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
3957

4058
@RestController
4159
@ApiResponse(
@@ -168,6 +186,40 @@ public String getLog(@RequestParam(name = "period", required = false) String per
168186
}
169187
}
170188

189+
@GetMapping(
190+
path = "/admin/logs",
191+
produces = MediaType.APPLICATION_JSON_VALUE
192+
)
193+
public ResponseEntity<Page<PersistedLogJson>> getLog(
194+
Pageable pageable,
195+
@RequestParam(name = "period", required = false) String periodString
196+
) {
197+
try {
198+
admins.checkAdminUser();
199+
200+
Page<PersistedLog> logsPage;
201+
if (StringUtils.isEmpty(periodString)) {
202+
logsPage = repositories.findPersistedLogsPaginated(pageable);
203+
} else {
204+
try {
205+
var period = Period.parse(periodString);
206+
var now = TimeUtil.getCurrentUTC();
207+
logsPage = repositories.findPersistedLogsAfterPaginated(now.minus(period), pageable);
208+
} catch (DateTimeParseException _) {
209+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid period");
210+
}
211+
}
212+
213+
return ResponseEntity.ok(logsPage.map(log -> {
214+
var timestamp = log.getTimestamp().minusNanos(log.getTimestamp().getNano());
215+
return new PersistedLogJson(timestamp.toString(), log.getUser().getLoginName(), log.getMessage());
216+
}));
217+
} catch (ErrorResultException exc) {
218+
var status = exc.getStatus() != null ? exc.getStatus() : HttpStatus.BAD_REQUEST;
219+
throw new ResponseStatusException(status);
220+
}
221+
}
222+
171223
private String toString(PersistedLog log) {
172224
var timestamp = log.getTimestamp().minusNanos(log.getTimestamp().getNano());
173225
return timestamp + "\t" + log.getUser().getLoginName() + "\t" + log.getMessage();
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/******************************************************************************
2+
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* https://www.eclipse.org/legal/epl-2.0.
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*****************************************************************************/
13+
package org.eclipse.openvsx.json;
14+
15+
import com.fasterxml.jackson.annotation.JsonInclude;
16+
17+
@JsonInclude(JsonInclude.Include.NON_NULL)
18+
public record PersistedLogJson(String timestamp, String user, String message) {}

server/src/main/java/org/eclipse/openvsx/repositories/PersistedLogRepository.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import java.time.LocalDateTime;
1313

1414
import org.eclipse.openvsx.entities.PersistedLog;
15+
import org.springframework.data.domain.Page;
16+
import org.springframework.data.domain.Pageable;
1517
import org.springframework.data.repository.Repository;
1618
import org.springframework.data.util.Streamable;
1719

@@ -21,4 +23,7 @@ public interface PersistedLogRepository extends Repository<PersistedLog, Long> {
2123

2224
Streamable<PersistedLog> findByTimestampAfterOrderByTimestampAsc(LocalDateTime dateTime);
2325

26+
Page<PersistedLog> findAllByOrderByTimestampDesc(Pageable pageable);
27+
28+
Page<PersistedLog> findByTimestampAfterOrderByTimestampDesc(LocalDateTime dateTime, Pageable pageable);
2429
}

server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
import org.eclipse.openvsx.util.ExtensionId;
1717
import org.eclipse.openvsx.util.NamingUtil;
1818
import org.eclipse.openvsx.web.SitemapRow;
19-
import org.springframework.data.domain.*;
19+
import org.springframework.data.domain.Page;
20+
import org.springframework.data.domain.PageRequest;
21+
import org.springframework.data.domain.Pageable;
22+
import org.springframework.data.domain.Slice;
23+
import org.springframework.data.domain.Sort;
2024
import org.springframework.data.util.Streamable;
2125
import org.springframework.stereotype.Component;
2226

@@ -394,6 +398,14 @@ public Streamable<PersistedLog> findPersistedLogsAfter(LocalDateTime dateTime) {
394398
return persistedLogRepo.findByTimestampAfterOrderByTimestampAsc(dateTime);
395399
}
396400

401+
public Page<PersistedLog> findPersistedLogsPaginated(Pageable pageable) {
402+
return persistedLogRepo.findAllByOrderByTimestampDesc(pageable);
403+
}
404+
405+
public Page<PersistedLog> findPersistedLogsAfterPaginated(LocalDateTime dateTime, Pageable pageable) {
406+
return persistedLogRepo.findByTimestampAfterOrderByTimestampDesc(dateTime, pageable);
407+
}
408+
397409
public List<String> findAllSucceededDownloadCountProcessedItemsByStorageTypeAndNameIn(String storageType, List<String> names) {
398410
return downloadCountRepo.findAllSucceededDownloadCountProcessedItemsByStorageTypeAndNameIn(storageType, names);
399411
}

webui/src/extension-registry-service.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
LoginProviders, ScanResultJson, ScanCounts, ScanResultsResponse, ScanFilterOptions,
1616
FilesResponse, FileDecisionCountsJson, ScanDecisionRequest, ScanDecisionResponse,
1717
FileDecisionRequest, FileDecisionResponse, FileDecisionDeleteRequest, FileDecisionDeleteResponse,
18-
Tier, TierList, Customer, CustomerList, UsageStatsList,
18+
Tier, TierList, Customer, CustomerList, UsageStatsList, LogPageableList,
1919
} from './extension-registry-types';
2020
import { createAbsoluteURL, addQuery } from './utils';
2121
import { sendRequest, ErrorResponse } from './server-request';
@@ -509,6 +509,7 @@ export interface AdminService {
509509
updateCustomer(abortController: AbortController, name: string, customer: Customer): Promise<Readonly<Customer>>;
510510
deleteCustomer(abortController: AbortController, name: string): Promise<Readonly<SuccessResult | ErrorResult>>;
511511
getUsageStats(abortController: AbortController, customerName: string, date: Date): Promise<Readonly<UsageStatsList>>;
512+
getLogs(abortController: AbortController, page?: number, size?: number, period?: string): Promise<Readonly<LogPageableList>>;
512513
}
513514

514515
export interface AdminServiceConstructor {
@@ -964,6 +965,27 @@ export class AdminServiceImpl implements AdminService {
964965
credentials: true
965966
}, false);
966967
}
968+
969+
async getLogs(
970+
abortController: AbortController,
971+
page: number = 0,
972+
size: number = 20,
973+
period?: string
974+
): Promise<Readonly<LogPageableList>> {
975+
const query: { key: string, value: string | number }[] = [
976+
{ key: 'page', value: page },
977+
{ key: 'size', value: size }
978+
];
979+
if (period) {
980+
query.push({ key: 'period', value: period });
981+
}
982+
const endpoint = addQuery(createAbsoluteURL([this.registry.serverUrl, 'admin', 'logs']), query);
983+
return sendRequest({
984+
abortController,
985+
endpoint,
986+
credentials: true
987+
}, false);
988+
}
967989
}
968990

969991
export interface ExtensionFilter {

webui/src/extension-registry-types.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,3 +465,30 @@ export interface UsageStats {
465465
export interface UsageStatsList {
466466
stats: UsageStats[];
467467
}
468+
469+
export interface Log {
470+
timestamp: string;
471+
user: string;
472+
message: string;
473+
}
474+
475+
export interface LogPageableList {
476+
content: Log[];
477+
pageable: {
478+
pageNumber: number;
479+
pageSize: number;
480+
sort: { sorted: boolean; empty: boolean; unsorted: boolean };
481+
offset: number;
482+
paged: boolean;
483+
unpaged: boolean;
484+
};
485+
totalElements: number;
486+
totalPages: number;
487+
last: boolean;
488+
first: boolean;
489+
size: number;
490+
number: number;
491+
numberOfElements: number;
492+
sort: { sorted: boolean; empty: boolean; unsorted: boolean };
493+
empty: boolean;
494+
}

webui/src/pages/admin-dashboard/admin-dashboard.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ import { ScanAdmin } from './scan-admin';
2828
import SecurityIcon from '@mui/icons-material/Security';
2929
import StarIcon from '@mui/icons-material/Star';
3030
import BarChartIcon from '@mui/icons-material/BarChart';
31+
import HistoryIcon from '@mui/icons-material/History';
3132
import { Tiers } from './tiers/tiers';
3233
import { Customers } from './customers/customers';
3334
import { UsageStatsView } from './usage-stats/usage-stats';
35+
import { Logs } from './logs/logs';
3436
import { LoginComponent } from "../../default/login";
3537
import AccountBoxIcon from "@mui/icons-material/AccountBox";
3638

@@ -44,6 +46,7 @@ export namespace AdminDashboardRoutes {
4446
export const TIERS = createRoute([ROOT, 'tiers']);
4547
export const CUSTOMERS = createRoute([ROOT, 'customers']);
4648
export const USAGE_STATS = createRoute([ROOT, 'usage']);
49+
export const LOGS = createRoute([ROOT, 'logs']);
4750
}
4851

4952
const Message: FunctionComponent<{message: string}> = ({ message }) => {
@@ -77,6 +80,7 @@ export const AdminDashboard: FunctionComponent<AdminDashboardProps> = props => {
7780
<NavigationItem onOpenRoute={handleOpenRoute} active={currentPage === AdminDashboardRoutes.TIERS} label='Tiers' icon={<StarIcon />} route={AdminDashboardRoutes.TIERS} />
7881
<NavigationItem onOpenRoute={handleOpenRoute} active={currentPage === AdminDashboardRoutes.CUSTOMERS} label='Customers' icon={<PeopleIcon />} route={AdminDashboardRoutes.CUSTOMERS} />
7982
<NavigationItem onOpenRoute={handleOpenRoute} active={currentPage?.startsWith(AdminDashboardRoutes.USAGE_STATS)} label='Usage Stats' icon={<BarChartIcon />} route={AdminDashboardRoutes.USAGE_STATS} />
83+
<NavigationItem onOpenRoute={handleOpenRoute} active={currentPage === AdminDashboardRoutes.LOGS} label='Logs' icon={<HistoryIcon />} route={AdminDashboardRoutes.LOGS} />
8084
</Sidepanel>
8185
<Box
8286
overflow='auto'
@@ -111,6 +115,7 @@ export const AdminDashboard: FunctionComponent<AdminDashboardProps> = props => {
111115
<Route path='/customers' element={<Customers/>} />
112116
<Route path='/usage' element={<UsageStatsView/>} />
113117
<Route path='/usage/:customer' element={<UsageStatsView/>} />
118+
<Route path='/logs' element={<Logs/>} />
114119
<Route path='*' element={<Welcome/>} />
115120
</Routes>
116121
</Container>

webui/src/pages/admin-dashboard/components/data-grid-filter-operators.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/******************************************************************************
22
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
33
*
44
* See the NOTICE file(s) distributed with this work for additional
@@ -9,7 +9,7 @@
99
* https://www.eclipse.org/legal/epl-2.0.
1010
*
1111
* SPDX-License-Identifier: EPL-2.0
12-
*/
12+
*****************************************************************************/
1313

1414
import React, { FC } from 'react';
1515
import { Autocomplete, TextField } from '@mui/material';

webui/src/pages/admin-dashboard/components/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/******************************************************************************
22
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
33
*
44
* See the NOTICE file(s) distributed with this work for additional
@@ -9,7 +9,7 @@
99
* https://www.eclipse.org/legal/epl-2.0.
1010
*
1111
* SPDX-License-Identifier: EPL-2.0
12-
*/
12+
*****************************************************************************/
1313

1414
export {
1515
MultiSelectFilterInput,

webui/src/pages/admin-dashboard/customers/customer-form-dialog.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
/******************************************************************************
32
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
43
*

0 commit comments

Comments
 (0)