Skip to content
Merged
6 changes: 3 additions & 3 deletions server/src/main/java/org/eclipse/openvsx/UserAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import org.eclipse.openvsx.accesstoken.AccessTokenService;
Expand Down Expand Up @@ -241,7 +240,8 @@ public List<ExtensionJson> getOwnExtensions() {
}

var extVersions = repositories.findLatestVersions(user);
var types = new String[]{ DOWNLOAD, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST };

var types = new String[] { DOWNLOAD, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST };
var fileUrls = storageUtil.getFileUrls(extVersions, UrlUtil.getBaseUrl(), types);
return extVersions.stream()
.map(latest -> {
Expand All @@ -260,7 +260,7 @@ public List<ExtensionJson> getOwnExtensions() {

/**
* Add review/scan status information to the extension JSON.
*
* <p>
* This shows users the current state of their extension in simple terms:
* - "published" - Extension is active and publicly available
* - "under_review" - Extension is being reviewed (validation, scanning, etc.)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/******************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*****************************************************************************/
package org.eclipse.openvsx.json;

import io.swagger.v3.oas.annotations.media.Schema;

import static org.eclipse.openvsx.util.TargetPlatform.*;

/**
*
* @param targetPlatform Name of the target platform
* @param active Whether this target platform version is active
*/
@Schema(
name = "TargetPlatformActive",
description = "Target platform of an extension version and whether it is active"
)
public record TargetPlatformActiveJson(
@Schema(description = "Name of the target platform", allowableValues = {
NAME_WIN32_X64, NAME_WIN32_IA32, NAME_WIN32_ARM64,
NAME_LINUX_X64, NAME_LINUX_ARM64, NAME_LINUX_ARMHF,
NAME_ALPINE_X64, NAME_ALPINE_ARM64,
NAME_DARWIN_X64, NAME_DARWIN_ARM64,
NAME_WEB, NAME_UNIVERSAL
})
String targetPlatform,
@Schema(description = "Whether this extension version for this target platform is active")
boolean active
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
* ****************************************************************************** */
package org.eclipse.openvsx.json;

public record VersionTargetPlatformsJson(String version, String[] targetPlatforms) {}
import java.util.List;

public record VersionTargetPlatformsJson(String version, List<TargetPlatformActiveJson> targetPlatforms) {}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.apache.commons.lang3.StringUtils;
import org.eclipse.openvsx.entities.*;
import org.eclipse.openvsx.json.QueryRequest;
import org.eclipse.openvsx.json.TargetPlatformActiveJson;
import org.eclipse.openvsx.json.TargetPlatformVersionJson;
import org.eclipse.openvsx.json.VersionTargetPlatformsJson;
import org.eclipse.openvsx.util.TargetPlatform;
Expand Down Expand Up @@ -545,14 +546,20 @@ public List<VersionTargetPlatformsJson> findTargetPlatformsGroupedByVersion(Exte
EXTENSION_VERSION.UNIVERSAL_TARGET_PLATFORM.desc(),
EXTENSION_VERSION.TARGET_PLATFORM.asc()
);
var targetPlatformsActive = DSL.arrayAgg(EXTENSION_VERSION.ACTIVE)
.orderBy(
EXTENSION_VERSION.UNIVERSAL_TARGET_PLATFORM.desc(),
EXTENSION_VERSION.TARGET_PLATFORM.asc()
);

return dsl.select(
EXTENSION_VERSION.SEMVER_MAJOR,
EXTENSION_VERSION.SEMVER_MINOR,
EXTENSION_VERSION.SEMVER_PATCH,
EXTENSION_VERSION.SEMVER_IS_PRE_RELEASE,
EXTENSION_VERSION.VERSION,
targetPlatforms
targetPlatforms,
targetPlatformsActive
)
.from(EXTENSION_VERSION)
.where(EXTENSION_VERSION.EXTENSION_ID.eq(extension.getId()))
Expand All @@ -571,9 +578,10 @@ public List<VersionTargetPlatformsJson> findTargetPlatformsGroupedByVersion(Exte
EXTENSION_VERSION.VERSION.asc()
)
.fetch()
.map(row -> new VersionTargetPlatformsJson(
.map(row -> toVersionTargetPlatformsJson(
row.get(EXTENSION_VERSION.VERSION),
row.get(targetPlatforms)
row.get(targetPlatforms),
row.get(targetPlatformsActive)
));
}

Expand All @@ -583,14 +591,20 @@ public List<VersionTargetPlatformsJson> findTargetPlatformsGroupedByVersion(Exte
EXTENSION_VERSION.UNIVERSAL_TARGET_PLATFORM.desc(),
EXTENSION_VERSION.TARGET_PLATFORM.asc()
);
var targetPlatformsActive = DSL.arrayAgg(EXTENSION_VERSION.ACTIVE)
.orderBy(
EXTENSION_VERSION.UNIVERSAL_TARGET_PLATFORM.desc(),
EXTENSION_VERSION.TARGET_PLATFORM.asc()
);

return dsl.select(
EXTENSION_VERSION.SEMVER_MAJOR,
EXTENSION_VERSION.SEMVER_MINOR,
EXTENSION_VERSION.SEMVER_PATCH,
EXTENSION_VERSION.SEMVER_IS_PRE_RELEASE,
EXTENSION_VERSION.VERSION,
targetPlatforms
targetPlatforms,
targetPlatformsActive
)
.from(EXTENSION_VERSION)
.join(PERSONAL_ACCESS_TOKEN).on(PERSONAL_ACCESS_TOKEN.ID.eq(EXTENSION_VERSION.PUBLISHED_WITH_ID))
Expand All @@ -611,12 +625,22 @@ public List<VersionTargetPlatformsJson> findTargetPlatformsGroupedByVersion(Exte
EXTENSION_VERSION.VERSION.asc()
)
.fetch()
.map(row -> new VersionTargetPlatformsJson(
.map(row -> toVersionTargetPlatformsJson(
row.get(EXTENSION_VERSION.VERSION),
row.get(targetPlatforms)
row.get(targetPlatforms),
row.get(targetPlatformsActive)
));
}

private VersionTargetPlatformsJson toVersionTargetPlatformsJson(String version, String[] targetPlatforms, Boolean[] active) {
var platforms = new ArrayList<TargetPlatformActiveJson>(targetPlatforms.length);
for (int i = 0; i < targetPlatforms.length; i++) {
platforms.add(new TargetPlatformActiveJson(targetPlatforms[i], active[i]));
}

return new VersionTargetPlatformsJson(version, platforms);
}

public List<ExtensionVersion> findVersionsForUrls(Extension extension, String targetPlatform, String version) {
var query = dsl.selectQuery();
query.addSelect(
Expand Down
1 change: 1 addition & 0 deletions webui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This change log covers only the frontend library (webui) of Open VSX.
### Added

- Support searching users and managing their roles in the admin dashboard ([#1847](https://github.com/eclipse-openvsx/openvsx/pull/1847))
- Added an extension details page to admin dashboard and user settings ([#1939](https://github.com/eclipse-openvsx/openvsx/pull/1939))

### Changed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/******************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*****************************************************************************/

import { FunctionComponent, useContext, useState } from 'react';
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material';
import { ButtonWithProgress } from '../button-with-progress';
import { MainContext } from '../../context';
import { Extension, VersionTargetPlatforms } from '../../extension-registry-types';
import { VersionDeleteTarget } from './extension-version-delete-dialog';

export const DeleteAllVersionsDialog: FunctionComponent<DeleteAllVersionsDialogProps> = props => {
const { handleError } = useContext(MainContext);
const [working, setWorking] = useState(false);

const handleRemove = async () => {
try {
setWorking(true);
const targets: VersionDeleteTarget[] = props.versions.flatMap(v =>
v.targetPlatforms.map(({ targetPlatform }) => ({ version: v.version, targetPlatform }))
);
await props.onRemove(targets);
props.onDeleted();
props.onClose();
} catch (err) {
handleError(err);
} finally {
setWorking(false);
}
};

return (
<Dialog open={props.open} onClose={props.onClose} maxWidth='xs' fullWidth>
<DialogTitle>Delete all versions of {props.extension.displayName ?? props.extension.name}?</DialogTitle>
<DialogContent>
<DialogContentText>
This will permanently remove {props.versions.length} version
{props.versions.length === 1 ? '' : 's'} of this extension across all target platforms. This action
cannot be undone.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button variant='contained' onClick={props.onClose}>
Cancel
</Button>
<ButtonWithProgress sx={{ ml: 1 }} color='error' working={working} onClick={handleRemove}>
Delete All Versions
</ButtonWithProgress>
</DialogActions>
</Dialog>
);
};

export interface DeleteAllVersionsDialogProps {
open: boolean;
onClose: () => void;
extension: Extension;
versions: VersionTargetPlatforms[];
onRemove: (targets: VersionDeleteTarget[]) => Promise<unknown>;
onDeleted: () => void;
}
101 changes: 101 additions & 0 deletions webui/src/components/extension/extension-detail-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/******************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*****************************************************************************/

import { FunctionComponent, ReactNode, useEffect, useState } from 'react';
import { Box, Button, Divider, Stack, Typography } from '@mui/material';
import { Link as RouteLink } from 'react-router-dom';
import { Extension, VERSION_ALIASES, VersionTargetPlatforms } from '../../extension-registry-types';
import { ExtensionHeader } from './extension-header';
import { ExtensionStatusChips } from './extension-status-chips';
import { ExtensionVersionTable } from './extension-version-table';
import { DeleteVersionDialog, VersionDeleteTarget } from './extension-version-delete-dialog';
import { DeleteAllVersionsDialog } from './extension-delete-all-versions-dialog';
import { ExtensionDetailRoutes } from '../../pages/extension-detail/extension-detail-routes';
import { createRoute } from '../../utils';

export const ExtensionDetailView: FunctionComponent<ExtensionDetailViewProps> = props => {
Comment thread
netomi marked this conversation as resolved.
const { extension, actions, onRemoveVersion, onVersionDeleted } = props;

const [page, setPage] = useState(0);
const [deleteDialogVersion, setDeleteDialogVersion] = useState<VersionTargetPlatforms | null>(null);
const [deleteAllOpen, setDeleteAllOpen] = useState(false);

useEffect(() => {
setPage(0);
}, [extension]);

const publicRoute = createRoute([ExtensionDetailRoutes.ROOT, extension.namespace, extension.name]);
const allVersions = (extension.allTargetPlatformVersions ?? []).filter(v => !VERSION_ALIASES.includes(v.version));

return (
<Box>
<ExtensionHeader extension={extension} />
{extension.description && (
<Typography variant='body1' mb={2}>
{extension.description}
</Typography>
)}
<ExtensionStatusChips extension={extension} />
<Divider sx={{ my: 2 }} />
<Stack direction='row' spacing={2} mb={3}>
<Button variant='outlined' component={RouteLink} to={publicRoute}>
View in Marketplace
</Button>
<Button
variant='outlined'
color='error'
onClick={() => setDeleteAllOpen(true)}
disabled={allVersions.length === 0}>
Delete All Versions
</Button>
{actions}
</Stack>
<Typography variant='h6' gutterBottom>
Versions
</Typography>
<ExtensionVersionTable
versions={allVersions}
page={page}
onPageChange={setPage}
onDeleteVersion={setDeleteDialogVersion}
/>
{deleteDialogVersion && (
<DeleteVersionDialog
open={true}
onClose={() => setDeleteDialogVersion(null)}
extension={extension}
version={deleteDialogVersion}
onRemove={onRemoveVersion}
onDeleted={onVersionDeleted}
/>
)}
{deleteAllOpen && (
<DeleteAllVersionsDialog
open={true}
onClose={() => setDeleteAllOpen(false)}
extension={extension}
versions={allVersions}
onRemove={onRemoveVersion}
onDeleted={onVersionDeleted}
/>
)}
</Box>
);
};

export interface ExtensionDetailViewProps {
extension: Extension;
actions?: ReactNode;
onRemoveVersion: (targets: VersionDeleteTarget[]) => Promise<unknown>;
onVersionDeleted: () => void;
}
33 changes: 33 additions & 0 deletions webui/src/components/extension/extension-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/******************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*****************************************************************************/

import { FunctionComponent } from 'react';
import { Box, Typography } from '@mui/material';
import { Extension } from '../../extension-registry-types';
import { ExtensionIcon } from './extension-icon';

export const ExtensionHeader: FunctionComponent<ExtensionHeaderProps> = ({ extension }) => (
<Box display='flex' alignItems='center' gap={2} mb={2}>
<ExtensionIcon extension={extension} sx={{ width: '4rem', height: '4rem', objectFit: 'contain' }} />
<Box>
<Typography variant='h5'>{extension.displayName ?? extension.name}</Typography>
<Typography variant='body2' color='text.secondary'>
{extension.namespace}.{extension.name} &middot; v{extension.version}
</Typography>
</Box>
</Box>
);

export interface ExtensionHeaderProps {
extension: Extension;
}
Loading