diff --git a/app/src/main/java/io/apicurio/registry/rest/v2/GroupsResourceImpl.java b/app/src/main/java/io/apicurio/registry/rest/v2/GroupsResourceImpl.java index 29dbb2e3b0..fd2304a334 100644 --- a/app/src/main/java/io/apicurio/registry/rest/v2/GroupsResourceImpl.java +++ b/app/src/main/java/io/apicurio/registry/rest/v2/GroupsResourceImpl.java @@ -1012,7 +1012,8 @@ public VersionSearchResults listArtifactVersions(String groupId, String artifact limit = BigInteger.valueOf(20); } - VersionSearchResultsDto resultsDto = storage.searchVersions(defaultGroupIdToNull(groupId), artifactId, offset.intValue(), limit.intValue()); + VersionSearchResultsDto resultsDto = storage.searchVersions(defaultGroupIdToNull(groupId), + artifactId, OrderBy.createdOn, OrderDirection.asc, offset.intValue(), limit.intValue()); return V2ApiUtil.dtoToSearchResults(resultsDto); } diff --git a/app/src/main/java/io/apicurio/registry/rest/v3/GroupsResourceImpl.java b/app/src/main/java/io/apicurio/registry/rest/v3/GroupsResourceImpl.java index 311631c381..649847f275 100644 --- a/app/src/main/java/io/apicurio/registry/rest/v3/GroupsResourceImpl.java +++ b/app/src/main/java/io/apicurio/registry/rest/v3/GroupsResourceImpl.java @@ -22,6 +22,7 @@ import io.apicurio.registry.rest.v3.beans.ArtifactMetaData; import io.apicurio.registry.rest.v3.beans.ArtifactReference; import io.apicurio.registry.rest.v3.beans.ArtifactSearchResults; +import io.apicurio.registry.rest.v3.beans.ArtifactSortBy; import io.apicurio.registry.rest.v3.beans.Comment; import io.apicurio.registry.rest.v3.beans.CreateArtifact; import io.apicurio.registry.rest.v3.beans.CreateArtifactResponse; @@ -32,16 +33,16 @@ import io.apicurio.registry.rest.v3.beans.EditableVersionMetaData; import io.apicurio.registry.rest.v3.beans.GroupMetaData; import io.apicurio.registry.rest.v3.beans.GroupSearchResults; +import io.apicurio.registry.rest.v3.beans.GroupSortBy; import io.apicurio.registry.rest.v3.beans.HandleReferencesType; import io.apicurio.registry.rest.v3.beans.IfArtifactExists; -import io.apicurio.registry.rest.v3.beans.IfVersionExists; import io.apicurio.registry.rest.v3.beans.NewComment; import io.apicurio.registry.rest.v3.beans.Rule; -import io.apicurio.registry.rest.v3.beans.SortBy; import io.apicurio.registry.rest.v3.beans.SortOrder; import io.apicurio.registry.rest.v3.beans.VersionContent; import io.apicurio.registry.rest.v3.beans.VersionMetaData; import io.apicurio.registry.rest.v3.beans.VersionSearchResults; +import io.apicurio.registry.rest.v3.beans.VersionSortBy; import io.apicurio.registry.rest.v3.shared.CommonResourceOperations; import io.apicurio.registry.rules.RuleApplicationType; import io.apicurio.registry.rules.RulesService; @@ -248,9 +249,9 @@ public void updateGroupById(String groupId, EditableGroupMetaData data) { @Override @Authorized(style = AuthorizedStyle.None, level = AuthorizedLevel.Read) - public GroupSearchResults listGroups(BigInteger limit, BigInteger offset, SortOrder order, SortBy orderby) { + public GroupSearchResults listGroups(BigInteger limit, BigInteger offset, SortOrder order, GroupSortBy orderby) { if (orderby == null) { - orderby = SortBy.name; + orderby = GroupSortBy.groupId; } if (offset == null) { offset = BigInteger.valueOf(0); @@ -272,7 +273,7 @@ public GroupSearchResults listGroups(BigInteger limit, BigInteger offset, SortOr @Authorized(style = AuthorizedStyle.None, level = AuthorizedLevel.Write) public GroupMetaData createGroup(CreateGroup data) { GroupMetaDataDto.GroupMetaDataDtoBuilder group = GroupMetaDataDto.builder() - .groupId(data.getId()) + .groupId(data.getGroupId()) .description(data.getDescription()) .labels(data.getLabels()); @@ -281,7 +282,7 @@ public GroupMetaData createGroup(CreateGroup data) { storage.createGroup(group.build()); - return V3ApiUtil.groupDtoToGroup(storage.getGroupMetaData(data.getId())); + return V3ApiUtil.groupDtoToGroup(storage.getGroupMetaData(data.getGroupId())); } @Override @@ -614,11 +615,11 @@ public void updateArtifactVersionComment(String groupId, String artifactId, Stri @Override @Authorized(style = AuthorizedStyle.GroupOnly, level = AuthorizedLevel.Read) public ArtifactSearchResults listArtifactsInGroup(String groupId, BigInteger limit, BigInteger offset, - SortOrder order, SortBy orderby) { + SortOrder order, ArtifactSortBy orderby) { requireParameter("groupId", groupId); if (orderby == null) { - orderby = SortBy.name; + orderby = ArtifactSortBy.name; } if (offset == null) { offset = BigInteger.valueOf(0); @@ -770,13 +771,15 @@ public CreateArtifactResponse createArtifact(String groupId, IfArtifactExists if } } - @Override @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Read) - public VersionSearchResults listArtifactVersions(String groupId, String artifactId, BigInteger offset, BigInteger limit) { + public VersionSearchResults listArtifactVersions(String groupId, String artifactId, BigInteger offset, + BigInteger limit, SortOrder order, VersionSortBy orderby) { requireParameter("groupId", groupId); requireParameter("artifactId", artifactId); - + if (orderby == null) { + orderby = VersionSortBy.createdOn; + } if (offset == null) { offset = BigInteger.valueOf(0); } @@ -784,14 +787,18 @@ public VersionSearchResults listArtifactVersions(String groupId, String artifact limit = BigInteger.valueOf(20); } - VersionSearchResultsDto resultsDto = storage.searchVersions(new GroupId(groupId).getRawGroupIdWithNull(), artifactId, offset.intValue(), limit.intValue()); + final OrderBy oBy = OrderBy.valueOf(orderby.name()); + final OrderDirection oDir = order == null || order == SortOrder.desc ? OrderDirection.asc : OrderDirection.desc; + + VersionSearchResultsDto resultsDto = storage.searchVersions(new GroupId(groupId).getRawGroupIdWithNull(), + artifactId, oBy, oDir, offset.intValue(), limit.intValue()); return V3ApiUtil.dtoToSearchResults(resultsDto); } @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_IF_EXISTS}) + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID}) @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Write) - public VersionMetaData createArtifactVersion(String groupId, String artifactId, IfVersionExists ifExists, CreateVersion data) { + public VersionMetaData createArtifactVersion(String groupId, String artifactId, CreateVersion data) { requireParameter("content", data.getContent()); requireParameter("groupId", groupId); requireParameter("artifactId", artifactId); diff --git a/app/src/main/java/io/apicurio/registry/rest/v3/SearchResourceImpl.java b/app/src/main/java/io/apicurio/registry/rest/v3/SearchResourceImpl.java index 614ae668bc..db3d95c1bc 100644 --- a/app/src/main/java/io/apicurio/registry/rest/v3/SearchResourceImpl.java +++ b/app/src/main/java/io/apicurio/registry/rest/v3/SearchResourceImpl.java @@ -9,10 +9,13 @@ import io.apicurio.registry.metrics.health.readiness.ResponseTimeoutReadinessCheck; import io.apicurio.registry.model.GroupId; import io.apicurio.registry.rest.v3.beans.ArtifactSearchResults; -import io.apicurio.registry.rest.v3.beans.SortBy; +import io.apicurio.registry.rest.v3.beans.ArtifactSortBy; +import io.apicurio.registry.rest.v3.beans.GroupSearchResults; +import io.apicurio.registry.rest.v3.beans.GroupSortBy; import io.apicurio.registry.rest.v3.beans.SortOrder; import io.apicurio.registry.storage.RegistryStorage; import io.apicurio.registry.storage.dto.ArtifactSearchResultsDto; +import io.apicurio.registry.storage.dto.GroupSearchResultsDto; import io.apicurio.registry.storage.dto.OrderBy; import io.apicurio.registry.storage.dto.OrderDirection; import io.apicurio.registry.storage.dto.SearchFilter; @@ -51,15 +54,14 @@ public class SearchResourceImpl implements SearchResource { @Inject RegistryStorageContentUtils contentUtils; - @Override @Authorized(style=AuthorizedStyle.None, level=AuthorizedLevel.Read) public ArtifactSearchResults searchArtifacts(String name, BigInteger offset, BigInteger limit, SortOrder order, - SortBy orderby, List labels, String description, String group, - Long globalId, Long contentId) + ArtifactSortBy orderby, List labels, String description, String groupId, Long globalId, Long contentId, + String artifactId) { if (orderby == null) { - orderby = SortBy.name; + orderby = ArtifactSortBy.name; } if (offset == null) { offset = BigInteger.valueOf(0); @@ -69,7 +71,7 @@ public ArtifactSearchResults searchArtifacts(String name, BigInteger offset, Big } final OrderBy oBy = OrderBy.valueOf(orderby.name()); - final OrderDirection oDir = order == null || order == SortOrder.asc ? OrderDirection.asc : OrderDirection.desc; + final OrderDirection oDir = (order == null || order == SortOrder.asc) ? OrderDirection.asc : OrderDirection.desc; Set filters = new HashSet(); if (!StringUtil.isEmpty(name)) { @@ -78,8 +80,8 @@ public ArtifactSearchResults searchArtifacts(String name, BigInteger offset, Big if (!StringUtil.isEmpty(description)) { filters.add(SearchFilter.ofDescription(description)); } - if (!StringUtil.isEmpty(group)) { - filters.add(SearchFilter.ofGroup(new GroupId(group).getRawGroupIdWithNull())); + if (!StringUtil.isEmpty(groupId)) { + filters.add(SearchFilter.ofGroup(new GroupId(groupId).getRawGroupIdWithNull())); } if (labels != null && !labels.isEmpty()) { @@ -116,13 +118,13 @@ public ArtifactSearchResults searchArtifacts(String name, BigInteger offset, Big return V3ApiUtil.dtoToSearchResults(results); } - @Override @Authorized(style=AuthorizedStyle.None, level=AuthorizedLevel.Read) - public ArtifactSearchResults searchArtifactsByContent(Boolean canonical, String artifactType, BigInteger offset, BigInteger limit, SortOrder order, SortBy orderby, InputStream data) { + public ArtifactSearchResults searchArtifactsByContent(Boolean canonical, String artifactType, BigInteger offset, + BigInteger limit, SortOrder order, ArtifactSortBy orderby, InputStream data) { if (orderby == null) { - orderby = SortBy.name; + orderby = ArtifactSortBy.name; } if (offset == null) { offset = BigInteger.valueOf(0); @@ -158,6 +160,58 @@ public ArtifactSearchResults searchArtifactsByContent(Boolean canonical, String return V3ApiUtil.dtoToSearchResults(results); } + @Override + public GroupSearchResults searchGroups(BigInteger offset, BigInteger limit, SortOrder order, GroupSortBy orderby, + List labels, String description, String groupId) { + if (orderby == null) { + orderby = GroupSortBy.groupId; + } + if (offset == null) { + offset = BigInteger.valueOf(0); + } + if (limit == null) { + limit = BigInteger.valueOf(20); + } + + final OrderBy oBy = OrderBy.valueOf(orderby.name()); + final OrderDirection oDir = order == null || order == SortOrder.asc ? OrderDirection.asc : OrderDirection.desc; + + Set filters = new HashSet(); + if (!StringUtil.isEmpty(groupId)) { + filters.add(SearchFilter.ofGroup(groupId)); + } + if (!StringUtil.isEmpty(description)) { + filters.add(SearchFilter.ofDescription(description)); + } + + if (labels != null && !labels.isEmpty()) { + labels.stream() + .map(prop -> { + int delimiterIndex = prop.indexOf(":"); + String labelKey; + String labelValue; + if (delimiterIndex == 0) { + throw new BadRequestException("label search filter wrong formatted, missing left side of ':' delimiter"); + } + if (delimiterIndex == (prop.length() - 1)) { + throw new BadRequestException("label search filter wrong formatted, missing right side of ':' delimiter"); + } + if (delimiterIndex < 0) { + labelKey = prop; + labelValue = null; + } else{ + labelKey = prop.substring(0, delimiterIndex); + labelValue = prop.substring(delimiterIndex + 1); + } + return SearchFilter.ofLabel(labelKey, labelValue); + }) + .forEach(filters::add); + } + + GroupSearchResultsDto results = storage.searchGroups(filters, oBy, oDir, offset.intValue(), limit.intValue()); + return V3ApiUtil.dtoToSearchResults(results); + } + /** * Make sure this is ONLY used when request instance is active. * e.g. in actual http request diff --git a/app/src/main/java/io/apicurio/registry/storage/RegistryStorage.java b/app/src/main/java/io/apicurio/registry/storage/RegistryStorage.java index c2647a7a5c..0a84e46540 100644 --- a/app/src/main/java/io/apicurio/registry/storage/RegistryStorage.java +++ b/app/src/main/java/io/apicurio/registry/storage/RegistryStorage.java @@ -347,7 +347,8 @@ void updateArtifactRule(String groupId, String artifactId, RuleType rule, RuleCo * @throws ArtifactNotFoundException * @throws RegistryStorageException */ - VersionSearchResultsDto searchVersions(String groupId, String artifactId, int offset, int limit) throws ArtifactNotFoundException, RegistryStorageException; + VersionSearchResultsDto searchVersions(String groupId, String artifactId, OrderBy orderBy, + OrderDirection orderDirection, int offset, int limit) throws ArtifactNotFoundException, RegistryStorageException; /** * Gets the stored artifact content for the artifact version with the given unique global ID. diff --git a/app/src/main/java/io/apicurio/registry/storage/decorator/RegistryStorageDecoratorReadOnlyBase.java b/app/src/main/java/io/apicurio/registry/storage/decorator/RegistryStorageDecoratorReadOnlyBase.java index 85790dd1a6..d55252b589 100644 --- a/app/src/main/java/io/apicurio/registry/storage/decorator/RegistryStorageDecoratorReadOnlyBase.java +++ b/app/src/main/java/io/apicurio/registry/storage/decorator/RegistryStorageDecoratorReadOnlyBase.java @@ -1,12 +1,5 @@ package io.apicurio.registry.storage.decorator; -import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; - import io.apicurio.common.apps.config.DynamicConfigPropertyDto; import io.apicurio.registry.content.ContentHandle; import io.apicurio.registry.model.BranchId; @@ -39,6 +32,13 @@ import io.apicurio.registry.types.RuleType; import io.apicurio.registry.utils.impexp.Entity; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + /** * Forwards all read-only method calls to the delegate. * @@ -160,11 +160,9 @@ public List getArtifactVersions(String groupId, String artifactId) return delegate.getArtifactVersions(groupId, artifactId); } - @Override - public VersionSearchResultsDto searchVersions(String groupId, String artifactId, int offset, int limit) - throws ArtifactNotFoundException, RegistryStorageException { - return delegate.searchVersions(groupId, artifactId, offset, limit); + public VersionSearchResultsDto searchVersions(String groupId, String artifactId, OrderBy orderBy, OrderDirection orderDirection, int offset, int limit) throws RegistryStorageException { + return delegate.searchVersions(groupId, artifactId, orderBy, orderDirection, offset, limit); } diff --git a/app/src/main/java/io/apicurio/registry/storage/dto/OrderBy.java b/app/src/main/java/io/apicurio/registry/storage/dto/OrderBy.java index 999f9ba12b..a2310ebfe7 100644 --- a/app/src/main/java/io/apicurio/registry/storage/dto/OrderBy.java +++ b/app/src/main/java/io/apicurio/registry/storage/dto/OrderBy.java @@ -1,5 +1,8 @@ package io.apicurio.registry.storage.dto; public enum OrderBy { - name, createdOn, globalId + name, createdOn, modifiedOn, // Shared + groupId, // Group specific + artifactId, artifactType, // Artifact specific + globalId, version // Version specific } diff --git a/app/src/main/java/io/apicurio/registry/storage/dto/SearchFilter.java b/app/src/main/java/io/apicurio/registry/storage/dto/SearchFilter.java index 0a3039350d..d5f6bbe6c6 100644 --- a/app/src/main/java/io/apicurio/registry/storage/dto/SearchFilter.java +++ b/app/src/main/java/io/apicurio/registry/storage/dto/SearchFilter.java @@ -66,10 +66,6 @@ public static SearchFilter ofState(VersionState state) { return new SearchFilter(SearchFilterType.state, state.name()); } - public static SearchFilter ofEverything(String value) { - return new SearchFilter(SearchFilterType.everything, value); - } - @SuppressWarnings("unchecked") public Pair getLabelFilterValue() { if (value == null) { diff --git a/app/src/main/java/io/apicurio/registry/storage/dto/SearchFilterType.java b/app/src/main/java/io/apicurio/registry/storage/dto/SearchFilterType.java index 413b8831f2..b5f88e7867 100644 --- a/app/src/main/java/io/apicurio/registry/storage/dto/SearchFilterType.java +++ b/app/src/main/java/io/apicurio/registry/storage/dto/SearchFilterType.java @@ -2,7 +2,6 @@ public enum SearchFilterType { - group, name, description, labels, contentHash, canonicalHash, - everything, globalId, contentId, state + group, name, description, labels, contentHash, canonicalHash, globalId, contentId, state } diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/gitops/GitOpsRegistryStorage.java b/app/src/main/java/io/apicurio/registry/storage/impl/gitops/GitOpsRegistryStorage.java index 598cf3db5d..77ae3a6d4a 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/gitops/GitOpsRegistryStorage.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/gitops/GitOpsRegistryStorage.java @@ -6,15 +6,30 @@ import io.apicurio.common.apps.logging.Logged; import io.apicurio.registry.content.ContentHandle; import io.apicurio.registry.metrics.StorageMetricsApply; +import io.apicurio.registry.model.BranchId; +import io.apicurio.registry.model.GA; +import io.apicurio.registry.model.GAV; import io.apicurio.registry.storage.RegistryStorage; -import io.apicurio.registry.storage.dto.*; +import io.apicurio.registry.storage.dto.ArtifactMetaDataDto; +import io.apicurio.registry.storage.dto.ArtifactReferenceDto; +import io.apicurio.registry.storage.dto.ArtifactSearchResultsDto; +import io.apicurio.registry.storage.dto.ArtifactVersionMetaDataDto; +import io.apicurio.registry.storage.dto.CommentDto; +import io.apicurio.registry.storage.dto.ContentWrapperDto; +import io.apicurio.registry.storage.dto.GroupMetaDataDto; +import io.apicurio.registry.storage.dto.GroupSearchResultsDto; +import io.apicurio.registry.storage.dto.OrderBy; +import io.apicurio.registry.storage.dto.OrderDirection; +import io.apicurio.registry.storage.dto.RoleMappingDto; +import io.apicurio.registry.storage.dto.RoleMappingSearchResultsDto; +import io.apicurio.registry.storage.dto.RuleConfigurationDto; +import io.apicurio.registry.storage.dto.SearchFilter; +import io.apicurio.registry.storage.dto.StoredArtifactVersionDto; +import io.apicurio.registry.storage.dto.VersionSearchResultsDto; import io.apicurio.registry.storage.error.RegistryStorageException; import io.apicurio.registry.storage.error.VersionNotFoundException; import io.apicurio.registry.storage.impl.gitops.sql.BlueSqlStorage; import io.apicurio.registry.storage.impl.gitops.sql.GreenSqlStorage; -import io.apicurio.registry.model.BranchId; -import io.apicurio.registry.model.GA; -import io.apicurio.registry.model.GAV; import io.apicurio.registry.types.RuleType; import io.apicurio.registry.utils.impexp.Entity; import io.quarkus.scheduler.Scheduled; @@ -267,10 +282,9 @@ public List getArtifactVersions(String groupId, String artifactId, Artif return proxy(storage -> storage.getArtifactVersions(groupId, artifactId, behavior)); } - @Override - public VersionSearchResultsDto searchVersions(String groupId, String artifactId, int offset, int limit) { - return proxy(storage -> storage.searchVersions(groupId, artifactId, offset, limit)); + public VersionSearchResultsDto searchVersions(String groupId, String artifactId, OrderBy orderBy, OrderDirection orderDirection, int offset, int limit) throws RegistryStorageException { + return proxy(storage -> storage.searchVersions(groupId, artifactId, orderBy, orderDirection, offset, limit)); } diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java index 9ac1ee61c8..c71d3c30c6 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java @@ -835,8 +835,10 @@ public ArtifactVersionMetaDataDto createArtifactVersion(String groupId, String a // Put the content in the DB and get the unique content ID back. long contentId = getOrCreateContent(handle, artifactType, content); + boolean isFirstVersion = countArtifactVersionsRaw(handle, groupId, artifactId) == 0; + // Now create the version and return the new version metadata. - ArtifactVersionMetaDataDto versionDto = createArtifactVersionRaw(handle, false, groupId, artifactId, version, + ArtifactVersionMetaDataDto versionDto = createArtifactVersionRaw(handle, isFirstVersion, groupId, artifactId, version, metaData == null ? EditableVersionMetaDataDto.builder().build() : metaData, owner, createdOn, contentId, branches); return versionDto; }); @@ -912,29 +914,6 @@ public ArtifactSearchResultsDto searchArtifacts(Set filters, Order query.bind(idx, "%" + filter.getStringValue() + "%"); }); break; - case everything: - where.append("a.name LIKE ? OR " - + "a.groupId LIKE ? OR " - + "a.artifactId LIKE ? OR " - + "a.description LIKE ? OR " - + "EXISTS(SELECT l.* FROM artifact_labels l WHERE l.labelKey = ? AND l.groupId = a.groupId AND l.artifactId = a.artifactId)"); - binders.add((query, idx) -> { - query.bind(idx, "%" + filter.getStringValue() + "%"); - }); - binders.add((query, idx) -> { - query.bind(idx, "%" + filter.getStringValue() + "%"); - }); - binders.add((query, idx) -> { - query.bind(idx, "%" + filter.getStringValue() + "%"); - }); - binders.add((query, idx) -> { - query.bind(idx, "%" + filter.getStringValue() + "%"); - }); - binders.add((query, idx) -> { - // Note: convert search to lowercase when searching for labels (case-insensitivity support). - query.bind(idx, filter.getStringValue().toLowerCase()); - }); - break; case name: op = filter.isNot() ? "NOT LIKE" : "LIKE"; where.append("a.name " + op + " ? OR a.artifactId " + op + " ?"); @@ -1024,11 +1003,20 @@ public ArtifactSearchResultsDto searchArtifacts(Set filters, Order case name: orderByQuery.append(" ORDER BY coalesce(a.name, a.artifactId)"); break; + case artifactId: + orderByQuery.append(" ORDER BY a.artifactId"); + break; case createdOn: orderByQuery.append(" ORDER BY a.createdOn"); break; - case globalId: - throw new RuntimeException("Sort by globalId no longer supported."); + case modifiedOn: + orderByQuery.append(" ORDER BY a.modifiedOn"); + break; + case artifactType: + orderByQuery.append(" ORDER BY a.type"); + break; + default: + throw new RuntimeException("Sort by " + orderBy.name() + " not supported."); } orderByQuery.append(" ").append(orderDirection.name()); @@ -1407,10 +1395,9 @@ public List getArtifactVersions(String groupId, String artifactId, Artif } } - @Override @Transactional - public VersionSearchResultsDto searchVersions(String groupId, String artifactId, int offset, int limit) { // TODO: Rename to differentiate from other search* methods. + public VersionSearchResultsDto searchVersions(String groupId, String artifactId, OrderBy orderBy, OrderDirection orderDirection, int offset, int limit) throws RegistryStorageException { // TODO: Rename to differentiate from other search* methods. log.debug("Searching for versions of artifact {} {}", groupId, artifactId); return handles.withHandleNoException(handle -> { VersionSearchResultsDto rval = new VersionSearchResultsDto(); @@ -1426,18 +1413,40 @@ public VersionSearchResultsDto searchVersions(String groupId, String artifactId, throw new ArtifactNotFoundException(groupId, artifactId); } - Query query = handle.createQuery(sqlStatements.selectAllArtifactVersions()) - .bind(0, normalizeGroupId(groupId)) - .bind(1, artifactId); + StringBuilder selectAllArtifactVersions = new StringBuilder(); + selectAllArtifactVersions.append(sqlStatements.selectAllArtifactVersions()); + selectAllArtifactVersions.append(" ORDER BY "); + switch (orderBy) { + case name: + selectAllArtifactVersions.append("v.name"); + break; + case createdOn: + selectAllArtifactVersions.append("v.createdOn"); + break; + case globalId: + selectAllArtifactVersions.append("v.globalId"); + break; + } + selectAllArtifactVersions.append(orderDirection == OrderDirection.asc ? " ASC " : " DESC "); if ("mssql".equals(sqlStatements.dbType())) { - query - .bind(2, offset) - .bind(3, limit); + // OFFSET ? ROWS FETCH NEXT ? ROWS ONLY + selectAllArtifactVersions.append("OFFSET "); + selectAllArtifactVersions.append(offset); + selectAllArtifactVersions.append(" ROWS FETCH NEXT "); + selectAllArtifactVersions.append(limit); + selectAllArtifactVersions.append("ROWS ONLY"); } else { - query - .bind(2, limit) - .bind(3, offset); + // LIMIT ? OFFSET ? + selectAllArtifactVersions.append("LIMIT "); + selectAllArtifactVersions.append(limit); + selectAllArtifactVersions.append(" OFFSET "); + selectAllArtifactVersions.append(offset); } + + Query query = handle.createQuery(selectAllArtifactVersions.toString()) + .bind(0, normalizeGroupId(groupId)) + .bind(1, artifactId); + List versions = query .map(SearchedVersionMapper.instance) .list(); @@ -1489,13 +1498,6 @@ public void deleteArtifactVersion(String groupId, String artifactId, String vers //For deleting artifact versions we need to list always every single version, including disabled ones. List versions = getArtifactVersions(groupId, artifactId, DEFAULT); - // If the version we're deleting is the *only* version, then just delete the - // entire artifact. - if (versions.size() == 1 && versions.iterator().next().equals(version)) { - deleteArtifact(groupId, artifactId); - return; - } - // If there is only one version, but it's not the version being deleted, then // we can't find the version to delete! This is an optimization. if (versions.size() == 1 && !versions.iterator().next().equals(version)) { @@ -1954,7 +1956,7 @@ public void createGroup(GroupMetaDataDto group) throws GroupAlreadyExistsExcepti // TODO io.apicurio.registry.storage.dto.GroupMetaDataDto should not use raw numeric timestamps .bind(4, group.getCreatedOn() == 0 ? new Date() : new Date(group.getCreatedOn())) .bind(5, group.getModifiedBy()) - .bind(6, group.getModifiedOn() == 0 ? null : new Date(group.getModifiedOn())) + .bind(6, group.getModifiedOn() == 0 ? new Date() : new Date(group.getModifiedOn())) .bind(7, SqlUtil.serializeLabels(group.getLabels())) .execute(); @@ -2224,15 +2226,16 @@ public long countArtifactVersions(String groupId, String artifactId) throws Regi throw new ArtifactNotFoundException(groupId, artifactId); } - return handles.withHandle(handle -> { - return handle.createQuery(sqlStatements.selectAllArtifactVersionsCount()) - .bind(0, normalizeGroupId(groupId)) - .bind(1, artifactId) - .mapTo(Long.class) - .one(); - }); + return handles.withHandle(handle -> countArtifactVersionsRaw(handle, groupId, artifactId)); } + protected long countArtifactVersionsRaw(Handle handle, String groupId, String artifactId) throws RegistryStorageException { + return handle.createQuery(sqlStatements.selectAllArtifactVersionsCount()) + .bind(0, normalizeGroupId(groupId)) + .bind(1, artifactId) + .mapTo(Long.class) + .one(); + } @Override @Transactional @@ -2572,6 +2575,7 @@ public boolean isArtifactVersionExists(String groupId, String artifactId, String public GroupSearchResultsDto searchGroups(Set filters, OrderBy orderBy, OrderDirection orderDirection, Integer offset, Integer limit) { return handles.withHandleNoException(handle -> { List binders = new LinkedList<>(); + String op; StringBuilder selectTemplate = new StringBuilder(); StringBuilder where = new StringBuilder(); @@ -2587,25 +2591,40 @@ public GroupSearchResultsDto searchGroups(Set filters, OrderBy ord where.append(" AND ("); switch (filter.getType()) { case description: - where.append("g.description LIKE ?"); + op = filter.isNot() ? "NOT LIKE" : "LIKE"; + where.append("g.description "); + where.append(op); + where.append(" ?"); binders.add((query, idx) -> { query.bind(idx, "%" + filter.getStringValue() + "%"); }); break; - case everything: - where.append("g.groupId LIKE ? OR g.description LIKE ?"); - binders.add((query, idx) -> { - query.bind(idx, "%" + filter.getStringValue() + "%"); - }); + case group: + op = filter.isNot() ? "NOT LIKE" : "LIKE"; + where.append("g.groupId "); + where.append(op); + where.append(" ?"); binders.add((query, idx) -> { query.bind(idx, "%" + filter.getStringValue() + "%"); }); break; - case group: - where.append("g.groupId = ?"); + case labels: + op = filter.isNot() ? "!=" : "="; + Pair label = filter.getLabelFilterValue(); + // Note: convert search to lowercase when searching for labels (case-insensitivity support). + String labelKey = label.getKey().toLowerCase(); + where.append("EXISTS(SELECT l.* FROM group_labels l WHERE l.labelKey " + op + " ?"); binders.add((query, idx) -> { - query.bind(idx, normalizeGroupId(filter.getStringValue())); + query.bind(idx, labelKey); }); + if (label.getValue() != null) { + String labelValue = label.getValue().toLowerCase(); + where.append(" AND l.labelValue " + op + " ?"); + binders.add((query, idx) -> { + query.bind(idx, labelValue); + }); + } + where.append(" AND l.groupId = g.groupId)"); break; default: break; @@ -2615,7 +2634,7 @@ public GroupSearchResultsDto searchGroups(Set filters, OrderBy ord // Add order by to artifact query switch (orderBy) { - case name: + case groupId: orderByQuery.append(" ORDER BY g.groupId"); break; case createdOn: diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/CommonSqlStatements.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/CommonSqlStatements.java index 58b1bf08b4..13f3c733f4 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/CommonSqlStatements.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/CommonSqlStatements.java @@ -346,14 +346,6 @@ public String deleteAllArtifactRules() { return "DELETE FROM artifact_rules"; } - /** - * @see io.apicurio.registry.storage.impl.sql.SqlStatements#updateArtifactVersionMetaData() - */ - @Override - public String updateArtifactVersionMetaData() { - return "UPDATE versions SET name = ?, description = ?, labels = ? WHERE groupId = ? AND artifactId = ? AND version = ?"; - } - /** * @see io.apicurio.registry.storage.impl.sql.SqlStatements#updateArtifactVersionNameByGAV() */ @@ -467,17 +459,6 @@ public String selectArtifactIds() { return "SELECT artifactId FROM artifacts LIMIT ?"; } - /** - * @see io.apicurio.registry.storage.impl.sql.SqlStatements#selectArtifactMetaDataByGlobalId() - */ - @Override - public String selectArtifactMetaDataByGlobalId() { - return "SELECT a.groupId, a.artifactId, a.type, a.owner, a.createdOn, v.contentId, v.globalId, v.version, v.versionOrder, v.state, v.name, v.description, v.labels, v.owner AS modifiedBy, v.createdOn AS modifiedOn " - + "FROM artifacts a " - + "JOIN versions v ON a.groupId = v.groupId AND a.artifactId = v.artifactId " - + "WHERE v.globalId = ?"; - } - /** * @see io.apicurio.registry.storage.impl.sql.SqlStatements#deleteVersion() */ @@ -525,8 +506,7 @@ public String insertGroupLabel() { public String selectAllArtifactVersions() { return "SELECT v.*, a.type FROM versions v " + "JOIN artifacts a ON a.groupId = v.groupId AND a.artifactId = v.artifactId " - + "WHERE a.groupId = ? AND a.artifactId = ? " - + "ORDER BY v.globalId ASC LIMIT ? OFFSET ?"; + + "WHERE a.groupId = ? AND a.artifactId = ?"; } /** @@ -542,9 +522,7 @@ public String selectAllArtifactCount() { */ @Override public String selectAllArtifactVersionsCount() { - return "SELECT COUNT(v.globalId) FROM versions v " - + "JOIN artifacts a ON a.groupId = v.groupId AND a.artifactId = v.artifactId " - + "WHERE a.groupId = ? AND a.artifactId = ? "; + return "SELECT COUNT(v.globalId) FROM versions v WHERE v.groupId = ? AND v.artifactId = ? "; } /** @@ -552,9 +530,7 @@ public String selectAllArtifactVersionsCount() { */ @Override public String selectActiveArtifactVersionsCount() { - return "SELECT COUNT(v.globalId) FROM versions v " - + "JOIN artifacts a ON a.groupId = v.groupId AND a.artifactId = v.artifactId " - + "WHERE a.groupId = ? AND a.artifactId = ? AND v.state != 'DISABLED'"; + return "SELECT COUNT(v.globalId) FROM versions v WHERE v.groupId = ? AND v.artifactId = ? AND v.state != 'DISABLED'"; } /** @@ -562,8 +538,7 @@ public String selectActiveArtifactVersionsCount() { */ @Override public String selectTotalArtifactVersionsCount() { - return "SELECT COUNT(v.globalId) FROM versions v " - + "JOIN artifacts a ON a.groupId = v.groupId AND a.artifactId = v.artifactId "; + return "SELECT COUNT(v.globalId) FROM versions v"; } /** @@ -727,7 +702,7 @@ public String exportVersionComments() { */ @Override public String exportContent() { - return "SELECT c.contentId, c.canonicalHash, c.contentHash, c.content, c.refs FROM content c "; + return "SELECT c.contentId, c.canonicalHash, c.contentHash, c.contentType, c.content, c.refs FROM content c "; } /** diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/SQLServerSqlStatements.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/SQLServerSqlStatements.java index 5b140bd0d8..770b15be5b 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/SQLServerSqlStatements.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/SQLServerSqlStatements.java @@ -115,17 +115,6 @@ public String selectArtifactIds() { return "SELECT TOP (?) artifactId FROM artifacts "; } - /** - * @see io.apicurio.registry.storage.impl.sql.SqlStatements#selectAllArtifactVersions() - */ - @Override - public String selectAllArtifactVersions() { - return "SELECT v.*, a.type FROM versions v " - + "JOIN artifacts a ON a.groupId = v.groupId AND a.artifactId = v.artifactId " - + "WHERE a.groupId = ? AND a.artifactId = ? " - + "ORDER BY v.globalId ASC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"; - } - /** * @see io.apicurio.registry.storage.impl.sql.SqlStatements#selectGroups() */ diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/SqlStatements.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/SqlStatements.java index 4cd449fcd9..2a2fae376e 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/SqlStatements.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/SqlStatements.java @@ -253,7 +253,6 @@ public interface SqlStatements { * Statements to update the meta-data of a specific artifact version. */ - public String updateArtifactVersionMetaData(); public String updateArtifactVersionNameByGAV(); public String updateArtifactVersionDescriptionByGAV(); public String updateArtifactVersionLabelsByGAV(); @@ -321,11 +320,6 @@ public interface SqlStatements { */ public String selectArtifactIds(); - /** - * A statement to get an artifact's meta-data by version globalId. - */ - public String selectArtifactMetaDataByGlobalId(); - /** * A statement to update the state of an artifact version (by globalId); */ diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ContentEntityMapper.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ContentEntityMapper.java index 074d069b65..bae4ccda43 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ContentEntityMapper.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ContentEntityMapper.java @@ -1,11 +1,11 @@ package io.apicurio.registry.storage.impl.sql.mappers; -import java.sql.ResultSet; -import java.sql.SQLException; - import io.apicurio.registry.storage.impl.sql.jdb.RowMapper; import io.apicurio.registry.utils.impexp.ContentEntity; +import java.sql.ResultSet; +import java.sql.SQLException; + public class ContentEntityMapper implements RowMapper { public static final ContentEntityMapper instance = new ContentEntityMapper(); @@ -23,6 +23,7 @@ private ContentEntityMapper() { public ContentEntity map(ResultSet rs) throws SQLException { ContentEntity entity = new ContentEntity(); entity.contentId = rs.getLong("contentId"); + entity.contentType = rs.getString("contentType"); entity.canonicalHash = rs.getString("canonicalHash"); entity.contentHash = rs.getString("contentHash"); entity.contentBytes = rs.getBytes("content"); diff --git a/app/src/test/java/io/apicurio/registry/AbstractResourceTestBase.java b/app/src/test/java/io/apicurio/registry/AbstractResourceTestBase.java index e6b2fb1732..d5b41dae4e 100644 --- a/app/src/test/java/io/apicurio/registry/AbstractResourceTestBase.java +++ b/app/src/test/java/io/apicurio/registry/AbstractResourceTestBase.java @@ -17,7 +17,6 @@ import io.apicurio.registry.types.ArtifactState; import io.apicurio.registry.types.ContentTypes; import io.apicurio.registry.types.RuleType; -import io.apicurio.registry.utils.ConcurrentUtil; import io.apicurio.registry.utils.tests.TestUtils; import io.apicurio.rest.client.auth.exception.NotAuthorizedException; import io.confluent.kafka.schemaregistry.client.rest.RestService; @@ -38,6 +37,7 @@ import java.util.Collections; import java.util.List; import java.util.UUID; +import java.util.function.Consumer; import java.util.stream.Collectors; import static org.hamcrest.Matchers.equalTo; @@ -120,7 +120,7 @@ protected CreateArtifactResponse createArtifact(String groupId, String artifactI } protected CreateArtifactResponse createArtifact(String groupId, String artifactId, String artifactType, String content, - String contentType, ConcurrentUtil.Function requestCustomizer) throws Exception { + String contentType, Consumer requestCustomizer) throws Exception { CreateArtifact createArtifact = new CreateArtifact(); createArtifact.setArtifactId(artifactId); createArtifact.setType(artifactType); @@ -132,7 +132,7 @@ protected CreateArtifactResponse createArtifact(String groupId, String artifactI versionContent.setContentType(contentType); if (requestCustomizer != null) { - requestCustomizer.apply(createArtifact); + requestCustomizer.accept(createArtifact); } var result = clientV3 diff --git a/app/src/test/java/io/apicurio/registry/auth/AuthTestAnonymousCredentials.java b/app/src/test/java/io/apicurio/registry/auth/AuthTestAnonymousCredentials.java index 0565fc3185..ab8054ab7c 100644 --- a/app/src/test/java/io/apicurio/registry/auth/AuthTestAnonymousCredentials.java +++ b/app/src/test/java/io/apicurio/registry/auth/AuthTestAnonymousCredentials.java @@ -53,7 +53,7 @@ public void testNoCredentials() throws Exception { adapter.setBaseUrl(registryV3ApiUrl); RegistryClient client = new RegistryClient(adapter); // Read-only operation should work without any credentials. - var results = client.search().artifacts().get(config -> config.queryParameters.group = groupId); + var results = client.search().artifacts().get(config -> config.queryParameters.groupId = groupId); Assertions.assertTrue(results.getCount() >= 0); // Write operation should fail without any credentials diff --git a/app/src/test/java/io/apicurio/registry/auth/AuthTestAuthenticatedReadAccess.java b/app/src/test/java/io/apicurio/registry/auth/AuthTestAuthenticatedReadAccess.java index 8b3ac48fed..a6f4b4f803 100644 --- a/app/src/test/java/io/apicurio/registry/auth/AuthTestAuthenticatedReadAccess.java +++ b/app/src/test/java/io/apicurio/registry/auth/AuthTestAuthenticatedReadAccess.java @@ -46,7 +46,7 @@ public void testReadOperationWithNoRole() throws Exception { var adapter = new VertXRequestAdapter(buildOIDCWebClient(authServerUrl, JWKSMockServer.NO_ROLE_CLIENT_ID, "test1")); adapter.setBaseUrl(registryV3ApiUrl); RegistryClient client = new RegistryClient(adapter); - var results = client.search().artifacts().get(config -> config.queryParameters.group = groupId); + var results = client.search().artifacts().get(config -> config.queryParameters.groupId = groupId); Assertions.assertTrue(results.getCount() >= 0); // Write operation should fail with credentials but not role. diff --git a/app/src/test/java/io/apicurio/registry/noprofile/ArtifactSearchTest.java b/app/src/test/java/io/apicurio/registry/noprofile/ArtifactSearchTest.java index 92830a900d..a04ac4ea41 100644 --- a/app/src/test/java/io/apicurio/registry/noprofile/ArtifactSearchTest.java +++ b/app/src/test/java/io/apicurio/registry/noprofile/ArtifactSearchTest.java @@ -2,9 +2,9 @@ import io.apicurio.registry.AbstractResourceTestBase; import io.apicurio.registry.rest.client.models.ArtifactSearchResults; +import io.apicurio.registry.rest.client.models.ArtifactSortBy; import io.apicurio.registry.rest.client.models.EditableArtifactMetaData; import io.apicurio.registry.rest.client.models.Labels; -import io.apicurio.registry.rest.client.models.SortBy; import io.apicurio.registry.rest.client.models.SortOrder; import io.apicurio.registry.types.ArtifactType; import io.apicurio.registry.types.ContentTypes; @@ -43,15 +43,14 @@ void testCaseInsensitiveSearch() throws Exception { createArtifact.setDescription(description); createArtifact.getFirstVersion().setName(title); createArtifact.getFirstVersion().setDescription(description); - return null; }); // Search against the name, with the exact name of the artifact ArtifactSearchResults results = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.name = title; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; }); @@ -69,9 +68,9 @@ void testCaseInsensitiveSearch() throws Exception { // Now try various cases when searching by labels ArtifactSearchResults ires = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.labels = new String[]{"testCaseInsensitiveSearchKey"}; @@ -79,9 +78,9 @@ void testCaseInsensitiveSearch() throws Exception { Assertions.assertNotNull(ires); Assertions.assertEquals(1, ires.getCount()); ires = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.labels = new String[]{"testCaseInsensitiveSearchKey".toLowerCase()}; @@ -89,9 +88,9 @@ void testCaseInsensitiveSearch() throws Exception { Assertions.assertNotNull(ires); Assertions.assertEquals(1, ires.getCount()); ires = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.labels = new String[]{"testCaseInsensitiveSearchKey".toUpperCase()}; @@ -99,9 +98,9 @@ void testCaseInsensitiveSearch() throws Exception { Assertions.assertNotNull(ires); Assertions.assertEquals(1, ires.getCount()); ires = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.labels = new String[]{"TESTCaseInsensitiveSEARCHKey"}; @@ -111,9 +110,9 @@ void testCaseInsensitiveSearch() throws Exception { // Now try various cases when searching by properties and values ArtifactSearchResults propertiesSearch = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.labels = new String[]{"testCaseInsensitiveSearchKey:testCaseInsensitiveSearchValue"}; @@ -121,9 +120,9 @@ void testCaseInsensitiveSearch() throws Exception { Assertions.assertNotNull(propertiesSearch); Assertions.assertEquals(1, propertiesSearch.getCount()); propertiesSearch = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.labels = new String[]{"testCaseInsensitiveSearchKey:testCaseInsensitiveSearchValue".toLowerCase()}; @@ -131,9 +130,9 @@ void testCaseInsensitiveSearch() throws Exception { Assertions.assertNotNull(propertiesSearch); Assertions.assertEquals(1, propertiesSearch.getCount()); propertiesSearch = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.labels = new String[]{"testCaseInsensitiveSearchKey:testCaseInsensitiveSearchValue".toUpperCase()}; @@ -141,9 +140,9 @@ void testCaseInsensitiveSearch() throws Exception { Assertions.assertNotNull(propertiesSearch); Assertions.assertEquals(1, propertiesSearch.getCount()); propertiesSearch = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.labels = new String[]{"TESTCaseInsensitiveSEARCHKey:TESTCaseInsensitiveSearchVALUE".toUpperCase()}; @@ -153,9 +152,9 @@ void testCaseInsensitiveSearch() throws Exception { // Now try various cases when searching by properties ArtifactSearchResults propertiesKeySearch = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.labels = new String[]{"testCaseInsensitiveSearchKey"}; @@ -163,9 +162,9 @@ void testCaseInsensitiveSearch() throws Exception { Assertions.assertNotNull(propertiesKeySearch); Assertions.assertEquals(1, propertiesKeySearch.getCount()); propertiesKeySearch = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.labels = new String[]{"testCaseInsensitiveSearchKey".toLowerCase()}; @@ -173,9 +172,9 @@ void testCaseInsensitiveSearch() throws Exception { Assertions.assertNotNull(propertiesKeySearch); Assertions.assertEquals(1, propertiesKeySearch.getCount()); propertiesKeySearch = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.labels = new String[]{"testCaseInsensitiveSearchKey".toUpperCase()}; @@ -183,9 +182,9 @@ void testCaseInsensitiveSearch() throws Exception { Assertions.assertNotNull(propertiesKeySearch); Assertions.assertEquals(1, propertiesKeySearch.getCount()); propertiesKeySearch = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.labels = new String[]{"TESTCaseInsensitiveSEARCHKey"}; diff --git a/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/EmptyArtifactTest.java b/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/EmptyArtifactTest.java new file mode 100644 index 0000000000..4894d1a201 --- /dev/null +++ b/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/EmptyArtifactTest.java @@ -0,0 +1,86 @@ +package io.apicurio.registry.noprofile.rest.v3; + +import io.apicurio.registry.AbstractResourceTestBase; +import io.apicurio.registry.rest.client.models.ArtifactMetaData; +import io.apicurio.registry.rest.client.models.CreateArtifact; +import io.apicurio.registry.rest.client.models.CreateVersion; +import io.apicurio.registry.rest.client.models.VersionMetaData; +import io.apicurio.registry.rest.client.models.VersionSearchResults; +import io.apicurio.registry.types.ArtifactType; +import io.apicurio.registry.types.ContentTypes; +import io.apicurio.registry.utils.tests.TestUtils; +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public class EmptyArtifactTest extends AbstractResourceTestBase { + + @Test + public void testCreateEmptyArtifact() throws Exception { + String groupId = TestUtils.generateGroupId(); + String artifactId = TestUtils.generateArtifactId(); + + CreateArtifact createArtifact = new CreateArtifact(); + createArtifact.setArtifactId(artifactId); + createArtifact.setType(ArtifactType.JSON); + + clientV3.groups().byGroupId(groupId).artifacts().post(createArtifact); + + ArtifactMetaData amd = clientV3.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).get(); + Assertions.assertNotNull(amd); + + VersionSearchResults versions = clientV3.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions().get(); + Assertions.assertNotNull(versions); + Assertions.assertEquals(0, versions.getCount()); + Assertions.assertEquals(0, versions.getVersions().size()); + } + + @Test + public void testCreateFirstVersion() throws Exception { + String groupId = TestUtils.generateGroupId(); + String artifactId = TestUtils.generateArtifactId(); + + CreateArtifact createArtifact = new CreateArtifact(); + createArtifact.setArtifactId(artifactId); + createArtifact.setType(ArtifactType.JSON); + + clientV3.groups().byGroupId(groupId).artifacts().post(createArtifact); + + CreateVersion createVersion = TestUtils.clientCreateVersion("{}", ContentTypes.APPLICATION_JSON); + clientV3.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions().post(createVersion); + + VersionMetaData vmd = clientV3.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions().byVersionExpression("1").get(); + Assertions.assertNotNull(vmd); + Assertions.assertEquals("1", vmd.getVersion()); + + vmd = clientV3.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions().byVersionExpression("branch=latest").get(); + Assertions.assertNotNull(vmd); + Assertions.assertEquals("1", vmd.getVersion()); + } + + @Test + public void testCreateFirstCustomVersion() throws Exception { + String groupId = TestUtils.generateGroupId(); + String artifactId = TestUtils.generateArtifactId(); + + CreateArtifact createArtifact = new CreateArtifact(); + createArtifact.setArtifactId(artifactId); + createArtifact.setType(ArtifactType.JSON); + + clientV3.groups().byGroupId(groupId).artifacts().post(createArtifact); + + CreateVersion createVersion = TestUtils.clientCreateVersion("{}", ContentTypes.APPLICATION_JSON); + createVersion.setVersion("1.0"); + clientV3.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions().post(createVersion); + + VersionMetaData vmd = clientV3.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions().byVersionExpression("1.0").get(); + Assertions.assertNotNull(vmd); + Assertions.assertEquals("1.0", vmd.getVersion()); + + vmd = clientV3.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions().byVersionExpression("branch=latest").get(); + Assertions.assertNotNull(vmd); + Assertions.assertEquals("1.0", vmd.getVersion()); + } + +} diff --git a/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/GroupMetaDataTest.java b/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/GroupMetaDataTest.java index 9ce19ac673..dfacee6235 100644 --- a/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/GroupMetaDataTest.java +++ b/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/GroupMetaDataTest.java @@ -24,7 +24,7 @@ public void createGroupWithMetadata() throws Exception { l.setAdditionalData(labels); CreateGroup body = new CreateGroup(); - body.setId(groupId); + body.setGroupId(groupId); body.setDescription("My favorite test group."); body.setLabels(l); GroupMetaData gmd = clientV3.groups().post(body); @@ -43,7 +43,7 @@ public void getGroupMetadata() throws Exception { l.setAdditionalData(labels); CreateGroup body = new CreateGroup(); - body.setId(groupId); + body.setGroupId(groupId); body.setDescription("My favorite test group."); body.setLabels(l); clientV3.groups().post(body); @@ -66,7 +66,7 @@ public void updateGroupMetadata() throws Exception { l.setAdditionalData(labels1); CreateGroup body = new CreateGroup(); - body.setId(groupId); + body.setGroupId(groupId); body.setDescription("My favorite test group."); body.setLabels(l); clientV3.groups().post(body); diff --git a/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/GroupsResourceTest.java b/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/GroupsResourceTest.java index 79ff542756..67eecb52e8 100644 --- a/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/GroupsResourceTest.java +++ b/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/GroupsResourceTest.java @@ -98,14 +98,14 @@ public void testDefaultGroup() throws Exception { // Search each group to ensure the correct # of artifacts. given() .when() - .queryParam("group", defaultGroup) + .queryParam("groupId", defaultGroup) .get("/registry/v3/search/artifacts") .then() .statusCode(200) .body("count", greaterThanOrEqualTo(5)); given() .when() - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -252,14 +252,14 @@ public void testMultipleGroups() throws Exception { // Search each group to ensure the correct # of artifacts. given() .when() - .queryParam("group", group1) + .queryParam("groupId", group1) .get("/registry/v3/search/artifacts") .then() .statusCode(200) .body("count", equalTo(5)); given() .when() - .queryParam("group", group2) + .queryParam("groupId", group2) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -917,7 +917,7 @@ public void testDeleteArtifactsInGroup() throws Exception { // Make sure we can search for all three artifacts in the group. given() .when() - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -934,7 +934,7 @@ public void testDeleteArtifactsInGroup() throws Exception { // Verify that all 3 artifacts were deleted given() .when() - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -954,7 +954,7 @@ public void testDeleteGroupWithArtifacts() throws Exception { // Make sure we can search for all three artifacts in the group. given() .when() - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -971,7 +971,7 @@ public void testDeleteGroupWithArtifacts() throws Exception { // Verify that all 3 artifacts were deleted given() .when() - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -1777,7 +1777,6 @@ public void testArtifactMetaData() throws Exception { createArtifact(GROUP, "testGetArtifactMetaData/EmptyAPI", ArtifactType.OPENAPI, artifactContent, ContentTypes.APPLICATION_JSON, (ca) -> { ca.setName("Empty API"); ca.setDescription("An example API design using OpenAPI."); - return null; }); // Get the artifact meta-data diff --git a/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/SearchResourceTest.java b/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/SearchArtifactsTest.java similarity index 85% rename from app/src/test/java/io/apicurio/registry/noprofile/rest/v3/SearchResourceTest.java rename to app/src/test/java/io/apicurio/registry/noprofile/rest/v3/SearchArtifactsTest.java index af20ecfe0d..f06b920825 100644 --- a/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/SearchResourceTest.java +++ b/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/SearchArtifactsTest.java @@ -15,10 +15,10 @@ import static org.hamcrest.Matchers.equalTo; @QuarkusTest -public class SearchResourceTest extends AbstractResourceTestBase { +public class SearchArtifactsTest extends AbstractResourceTestBase { @Test - public void testSearchByGroup() throws Exception { + public void testSearchArtifactsByGroup() throws Exception { String artifactContent = resourceToString("openapi-empty.json"); String group = UUID.randomUUID().toString(); @@ -29,7 +29,6 @@ public void testSearchByGroup() throws Exception { this.createArtifact(group, artifactId, ArtifactType.OPENAPI, artifactContent.replaceAll("Empty API", title), ContentTypes.APPLICATION_JSON, (ca) -> { ca.getFirstVersion().setName(title); - return null; }); } // Create 3 artifacts in some other group @@ -40,7 +39,7 @@ public void testSearchByGroup() throws Exception { given() .when() - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -50,7 +49,7 @@ public void testSearchByGroup() throws Exception { } @Test - public void testSearchByName() throws Exception { + public void testSearchArtifactsByName() throws Exception { String group = UUID.randomUUID().toString(); String name = UUID.randomUUID().toString(); String artifactContent = resourceToString("openapi-empty.json"); @@ -61,7 +60,6 @@ public void testSearchByName() throws Exception { this.createArtifact(group, artifactId, ArtifactType.OPENAPI, artifactContent.replaceAll("Empty API", name), ContentTypes.APPLICATION_JSON, (ca) -> { ca.setName(name); - return null; }); } // Three with a different name @@ -80,7 +78,7 @@ public void testSearchByName() throws Exception { } @Test - public void testSearchByDescription() throws Exception { + public void testSearchArtifactsByDescription() throws Exception { String group = UUID.randomUUID().toString(); String description = "The description is "+ UUID.randomUUID().toString(); String artifactContent = resourceToString("openapi-empty.json"); @@ -91,7 +89,6 @@ public void testSearchByDescription() throws Exception { this.createArtifact(group, artifactId, ArtifactType.OPENAPI, artifactContent.replaceAll("An example API design using OpenAPI.", description), ContentTypes.APPLICATION_JSON, (ca) -> { ca.setDescription(description); - return null; }); } // Three with the default description @@ -110,7 +107,7 @@ public void testSearchByDescription() throws Exception { } @Test - public void testSearchByLabels() throws Exception { + public void testSearchArtifactsByLabels() throws Exception { String group = UUID.randomUUID().toString(); String artifactContent = resourceToString("openapi-empty.json"); @@ -238,41 +235,7 @@ public void testSearchByLabels() throws Exception { } @Test - public void testSearchByPropertyKey() throws Exception { - String group = UUID.randomUUID().toString(); - String artifactContent = resourceToString("openapi-empty.json"); - - // Create 5 artifacts with various labels - for (int idx = 0; idx < 5; idx++) { - String title = "Empty API " + idx; - String artifactId = "Empty-" + idx; - this.createArtifact(group, artifactId, ArtifactType.OPENAPI, artifactContent.replaceAll("Empty API", title), ContentTypes.APPLICATION_JSON); - - Map labels = new HashMap<>(); - labels.put("all-key", "lorem ipsum"); - labels.put("a-key-" + idx, "lorem ipsum"); - labels.put("an-another-key-" + idx, "lorem ipsum"); - labels.put("extra-key-" + (idx % 2), "lorem ipsum"); - - // Update the artifact meta-data - EditableArtifactMetaData metaData = new EditableArtifactMetaData(); - metaData.setName(title); - metaData.setDescription("Some description of an API"); - metaData.setLabels(labels); - given() - .when() - .contentType(CT_JSON) - .pathParam("groupId", group) - .pathParam("artifactId", artifactId) - .body(metaData) - .put("/registry/v3/groups/{groupId}/artifacts/{artifactId}") - .then() - .statusCode(204); - } - } - - @Test - public void testOrderBy() throws Exception { + public void testSearchArtifactsOrderBy() throws Exception { String group = UUID.randomUUID().toString(); String artifactContent = resourceToString("openapi-empty.json"); @@ -283,7 +246,6 @@ public void testOrderBy() throws Exception { ContentTypes.APPLICATION_JSON, (ca) -> { ca.setName(name); ca.getFirstVersion().setName(name); - return null; }); } @@ -291,7 +253,7 @@ public void testOrderBy() throws Exception { .when() .queryParam("orderby", "name") .queryParam("order", "asc") - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -302,7 +264,7 @@ public void testOrderBy() throws Exception { .when() .queryParam("orderby", "name") .queryParam("order", "desc") - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -313,7 +275,7 @@ public void testOrderBy() throws Exception { .when() .queryParam("orderby", "createdOn") .queryParam("order", "asc") - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -324,7 +286,7 @@ public void testOrderBy() throws Exception { .when() .queryParam("orderby", "createdOn") .queryParam("order", "desc") - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -333,7 +295,7 @@ public void testOrderBy() throws Exception { } @Test - public void testLimitAndOffset() throws Exception { + public void testSearchArtifactsLimitAndOffset() throws Exception { String group = UUID.randomUUID().toString(); String artifactContent = resourceToString("openapi-empty.json"); @@ -344,7 +306,6 @@ public void testLimitAndOffset() throws Exception { (ca) -> { ca.setName(name); ca.getFirstVersion().setName(name); - return null; }); } @@ -353,7 +314,7 @@ public void testLimitAndOffset() throws Exception { .queryParam("orderby", "createdOn") .queryParam("order", "asc") .queryParam("limit", 5) - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -366,7 +327,7 @@ public void testLimitAndOffset() throws Exception { .queryParam("orderby", "createdOn") .queryParam("order", "asc") .queryParam("limit", 15) - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -380,7 +341,7 @@ public void testLimitAndOffset() throws Exception { .queryParam("order", "asc") .queryParam("limit", 5) .queryParam("offset", 10) - .queryParam("group", group) + .queryParam("groupId", group) .get("/registry/v3/search/artifacts") .then() .statusCode(200) @@ -391,7 +352,7 @@ public void testLimitAndOffset() throws Exception { } @Test - public void testSearchByContent() throws Exception { + public void testSearchArtifactsByContent() throws Exception { String artifactContent = resourceToString("openapi-empty.json"); String group = "testSearchByContent"; String searchByContent = artifactContent.replaceAll("Empty API", "testSearchByContent-empty-api-2"); @@ -429,7 +390,7 @@ public void testSearchByContent() throws Exception { @Test - public void testSearchByCanonicalContent() throws Exception { + public void testSearchArtifactsByCanonicalContent() throws Exception { String artifactContent = resourceToString("openapi-empty.json"); String group = "testSearchByCanonicalContent"; String searchByContent = artifactContent.replaceAll("Empty API", "testSearchByCanonicalContent-empty-api-2").replaceAll("\\{", " {\n"); diff --git a/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/SearchGroupsTest.java b/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/SearchGroupsTest.java new file mode 100644 index 0000000000..31366a1a8e --- /dev/null +++ b/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/SearchGroupsTest.java @@ -0,0 +1,115 @@ +package io.apicurio.registry.noprofile.rest.v3; + +import io.apicurio.registry.AbstractResourceTestBase; +import io.apicurio.registry.rest.client.models.CreateGroup; +import io.apicurio.registry.rest.client.models.GroupSearchResults; +import io.apicurio.registry.rest.client.models.Labels; +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +@QuarkusTest +public class SearchGroupsTest extends AbstractResourceTestBase { + + @Test + public void testSearchGroupsByName() throws Exception { + // Create 5 groups + for (int idx = 0; idx < 5; idx++) { + String groupId = "testSearchGroupsByName" + idx; + CreateGroup createGroup = new CreateGroup(); + createGroup.setGroupId(groupId); + clientV3.groups().post(createGroup); + } + + GroupSearchResults results = clientV3.search().groups().get(request -> { + request.queryParameters.groupId = "testSearchGroupsByName"; + }); + Assertions.assertEquals(5, results.getGroups().size()); + + results = clientV3.search().groups().get(request -> { + request.queryParameters.groupId = "testSearchGroupsByName3"; + }); + Assertions.assertEquals(1, results.getGroups().size()); + Assertions.assertEquals("testSearchGroupsByName3", results.getGroups().get(0).getGroupId()); + } + + @Test + public void testSearchGroupsByDescription() throws Exception { + // Create 5 groups + for (int idx = 0; idx < 5; idx++) { + String groupId = "testSearchGroupsByDescription" + idx; + String description = "Description of group number " + idx; + CreateGroup createGroup = new CreateGroup(); + createGroup.setGroupId(groupId); + createGroup.setDescription(description); + clientV3.groups().post(createGroup); + } + + GroupSearchResults results = clientV3.search().groups().get(request -> { + request.queryParameters.groupId = "testSearchGroupsByDescription"; + }); + Assertions.assertEquals(5, results.getGroups().size()); + + results = clientV3.search().groups().get(request -> { + request.queryParameters.description = "Description of group number 3"; + }); + Assertions.assertEquals(1, results.getGroups().size()); + Assertions.assertEquals("testSearchGroupsByDescription3", results.getGroups().get(0).getGroupId()); + Assertions.assertEquals("Description of group number 3", results.getGroups().get(0).getDescription()); + } + + @Test + public void testSearchGroupsByLabels() throws Exception { + // Create 5 groups + for (int idx = 0; idx < 5; idx++) { + String groupId = "testSearchGroupsByLabels" + idx; + Labels labels = new Labels(); + labels.setAdditionalData(Map.of( + "byLabels", "byLabels-value-" + idx, + "byLabels-" + idx, "byLabels-value-" + idx + )); + + CreateGroup createGroup = new CreateGroup(); + createGroup.setGroupId(groupId); + createGroup.setLabels(labels); + clientV3.groups().post(createGroup); + } + + GroupSearchResults results = clientV3.search().groups().get(request -> { + request.queryParameters.groupId = "testSearchGroupsByLabels"; + }); + Assertions.assertEquals(5, results.getGroups().size()); + + results = clientV3.search().groups().get(request -> { + request.queryParameters.labels = new String[]{ "byLabels" }; + }); + Assertions.assertEquals(5, results.getGroups().size()); + + results = clientV3.search().groups().get(request -> { + request.queryParameters.labels = new String[]{ "byLabels-3" }; + }); + Assertions.assertEquals(1, results.getGroups().size()); + Assertions.assertEquals("testSearchGroupsByLabels3", results.getGroups().get(0).getGroupId()); + + results = clientV3.search().groups().get(request -> { + request.queryParameters.labels = new String[]{ "byLabels:byLabels-value-3" }; + }); + Assertions.assertEquals(1, results.getGroups().size()); + Assertions.assertEquals("testSearchGroupsByLabels3", results.getGroups().get(0).getGroupId()); + + results = clientV3.search().groups().get(request -> { + request.queryParameters.labels = new String[]{ "byLabels-3" }; + }); + Assertions.assertEquals(1, results.getGroups().size()); + Assertions.assertEquals("testSearchGroupsByLabels3", results.getGroups().get(0).getGroupId()); + + results = clientV3.search().groups().get(request -> { + request.queryParameters.labels = new String[]{ "byLabels-3:byLabels-value-3" }; + }); + Assertions.assertEquals(1, results.getGroups().size()); + Assertions.assertEquals("testSearchGroupsByLabels3", results.getGroups().get(0).getGroupId()); + } + +} diff --git a/app/src/test/java/io/apicurio/registry/noprofile/storage/RegistryStoragePerformanceTest.java b/app/src/test/java/io/apicurio/registry/noprofile/storage/RegistryStoragePerformanceTest.java index ee79e36505..1af5ad2059 100644 --- a/app/src/test/java/io/apicurio/registry/noprofile/storage/RegistryStoragePerformanceTest.java +++ b/app/src/test/java/io/apicurio/registry/noprofile/storage/RegistryStoragePerformanceTest.java @@ -140,13 +140,6 @@ public void testStoragePerformance() throws Exception { Assertions.assertNotNull(results); Assertions.assertEquals(NUM_ARTIFACTS, results.getCount()); - long startEverythingSearch = System.currentTimeMillis(); - filters = Collections.singleton(SearchFilter.ofEverything("test")); - results = storage.searchArtifacts(filters, OrderBy.name, OrderDirection.asc, 0, 10); - long endEverythingSearch = System.currentTimeMillis(); - Assertions.assertNotNull(results); - Assertions.assertEquals(NUM_ARTIFACTS, results.getCount()); - System.out.println("========================================================================"); System.out.println("= Storage Performance Results ="); System.out.println("=----------------------------------------------------------------------="); @@ -160,7 +153,6 @@ public void testStoragePerformance() throws Exception { System.out.println("| All Name Search: " + (endAllNameSearch - startAllNameSearch) + "ms"); System.out.println("| Label Search: " + (endLabelSearch - startLabelSearch) + "ms"); System.out.println("| All Label Search: " + (endAllLabelSearch - startAllLabelSearch) + "ms"); - System.out.println("| Everything Search: " + (endEverythingSearch - startEverythingSearch) + "ms"); System.out.println("========================================================================"); } diff --git a/app/src/test/java/io/apicurio/registry/rbac/AdminResourceTest.java b/app/src/test/java/io/apicurio/registry/rbac/AdminResourceTest.java index eeee260009..89101c0b96 100644 --- a/app/src/test/java/io/apicurio/registry/rbac/AdminResourceTest.java +++ b/app/src/test/java/io/apicurio/registry/rbac/AdminResourceTest.java @@ -407,7 +407,7 @@ public void testCompatilibityLevelNone() throws Exception { @Test void testExport() throws Exception { String artifactContent = resourceToString("openapi-empty.json"); - String group = "testExport"; + String group = TestUtils.generateGroupId(); // Create 5 artifacts in the UUID group for (int idx = 0; idx < 5; idx++) { @@ -424,6 +424,9 @@ void testExport() throws Exception { InputStream body = response.extract().asInputStream(); ZipInputStream zip = new ZipInputStream(body); + int artifactCount = clientV3.groups().byGroupId(group).artifacts().get().getCount(); + Assertions.assertEquals(5, artifactCount); + AtomicInteger contentCounter = new AtomicInteger(0); AtomicInteger versionCounter = new AtomicInteger(0); diff --git a/app/src/test/java/io/apicurio/registry/rbac/RegistryClientTest.java b/app/src/test/java/io/apicurio/registry/rbac/RegistryClientTest.java index d904bbc321..36732e9a5f 100644 --- a/app/src/test/java/io/apicurio/registry/rbac/RegistryClientTest.java +++ b/app/src/test/java/io/apicurio/registry/rbac/RegistryClientTest.java @@ -8,6 +8,7 @@ import io.apicurio.registry.rest.client.models.ArtifactMetaData; import io.apicurio.registry.rest.client.models.ArtifactReference; import io.apicurio.registry.rest.client.models.ArtifactSearchResults; +import io.apicurio.registry.rest.client.models.ArtifactSortBy; import io.apicurio.registry.rest.client.models.ConfigurationProperty; import io.apicurio.registry.rest.client.models.CreateArtifact; import io.apicurio.registry.rest.client.models.CreateArtifactResponse; @@ -17,6 +18,7 @@ import io.apicurio.registry.rest.client.models.EditableVersionMetaData; import io.apicurio.registry.rest.client.models.GroupMetaData; import io.apicurio.registry.rest.client.models.GroupSearchResults; +import io.apicurio.registry.rest.client.models.GroupSortBy; import io.apicurio.registry.rest.client.models.Labels; import io.apicurio.registry.rest.client.models.RoleMapping; import io.apicurio.registry.rest.client.models.RoleType; @@ -24,7 +26,6 @@ import io.apicurio.registry.rest.client.models.RuleType; import io.apicurio.registry.rest.client.models.SearchedArtifact; import io.apicurio.registry.rest.client.models.SearchedGroup; -import io.apicurio.registry.rest.client.models.SortBy; import io.apicurio.registry.rest.client.models.SortOrder; import io.apicurio.registry.rest.client.models.UpdateConfigurationProperty; import io.apicurio.registry.rest.client.models.UpdateRole; @@ -139,7 +140,6 @@ public void testCreateArtifact() throws Exception { createArtifact.setName(name); createArtifact.setDescription(description); createArtifact.getFirstVersion().setVersion(version); - return null; })); //Assertions @@ -157,7 +157,7 @@ public void groupsCrud() throws Exception { //Preparation final String groupId = UUID.randomUUID().toString(); CreateGroup groupMetaData = new CreateGroup(); - groupMetaData.setId(groupId); + groupMetaData.setGroupId(groupId); groupMetaData.setDescription("Groups test crud"); Labels labels = new Labels(); labels.setAdditionalData(Map.of("p1", "v1", "p2", "v2")); @@ -166,7 +166,7 @@ public void groupsCrud() throws Exception { clientV3.groups().post(groupMetaData); final GroupMetaData artifactGroup = clientV3.groups().byGroupId(groupId).get(); - assertEquals(groupMetaData.getId(), artifactGroup.getGroupId()); + assertEquals(groupMetaData.getGroupId(), artifactGroup.getGroupId()); assertEquals(groupMetaData.getDescription(), artifactGroup.getDescription()); assertEquals(groupMetaData.getLabels().getAdditionalData(), artifactGroup.getLabels().getAdditionalData()); @@ -175,11 +175,11 @@ public void groupsCrud() throws Exception { String group2Id = UUID.randomUUID().toString(); String group3Id = UUID.randomUUID().toString(); - groupMetaData.setId(group1Id); + groupMetaData.setGroupId(group1Id); clientV3.groups().post(groupMetaData); - groupMetaData.setId(group2Id); + groupMetaData.setGroupId(group2Id); clientV3.groups().post(groupMetaData); - groupMetaData.setId(group3Id); + groupMetaData.setGroupId(group3Id); clientV3.groups().post(groupMetaData); @@ -187,7 +187,7 @@ public void groupsCrud() throws Exception { config.queryParameters.offset = 0; config.queryParameters.limit = 100; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = GroupSortBy.GroupId; }); assertTrue(groupSearchResults.getCount() >= 4); @@ -358,10 +358,10 @@ public void testSmoke() throws Exception { //Execution final ArtifactSearchResults searchResults = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.offset = 0; config.queryParameters.limit = 2; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.order = SortOrder.Asc; }); @@ -377,10 +377,10 @@ public void testSmoke() throws Exception { { //Execution final ArtifactSearchResults deletedResults = clientV3.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.offset = 0; config.queryParameters.limit = 2; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.order = SortOrder.Asc; }); //Assertion @@ -409,7 +409,7 @@ void testSearchArtifact() throws Exception { config.queryParameters.name = name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.order = SortOrder.Asc; }); @@ -457,7 +457,7 @@ void testSearchArtifactSortByCreatedOn() throws Exception { config.queryParameters.name = name; config.queryParameters.offset = 0; config.queryParameters.limit = 10; - config.queryParameters.orderby = SortBy.CreatedOn; + config.queryParameters.orderby = ArtifactSortBy.CreatedOn; config.queryParameters.order = SortOrder.Asc; }); @@ -504,7 +504,7 @@ void testSearchArtifactByIds() throws Exception { config.queryParameters.globalId = amd.getGlobalId(); config.queryParameters.offset = 0; config.queryParameters.limit = 10; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.order = SortOrder.Asc; }); @@ -518,7 +518,7 @@ void testSearchArtifactByIds() throws Exception { config.queryParameters.contentId = amd.getContentId(); config.queryParameters.offset = 0; config.queryParameters.limit = 10; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.order = SortOrder.Asc; }); @@ -587,11 +587,11 @@ void testSearchDisabledArtifacts() throws Exception { config.queryParameters.name = root; config.queryParameters.offset = 0; config.queryParameters.limit = 10; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.order = SortOrder.Asc; }); -// clientV2.searchArtifacts(null, root, null, null, null, SortBy.name, SortOrder.asc, 0, 10); +// clientV2.searchArtifacts(null, root, null, null, null, ArtifactSortBy.name, SortOrder.asc, 0, 10); //Assertions Assertions.assertNotNull(results); @@ -614,7 +614,7 @@ void testSearchDisabledArtifacts() throws Exception { config.queryParameters.name = root; config.queryParameters.offset = 0; config.queryParameters.limit = 10; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.order = SortOrder.Asc; }); @@ -769,8 +769,8 @@ void nameOrderingTest() throws Exception { config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.name = "Testorder"; - config.queryParameters.group = groupId; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.groupId = groupId; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.order = SortOrder.Asc; }); @@ -787,8 +787,8 @@ void nameOrderingTest() throws Exception { config.queryParameters.offset = 0; config.queryParameters.limit = 10; config.queryParameters.name = "Testorder"; - config.queryParameters.group = groupId; - config.queryParameters.orderby = SortBy.Name; + config.queryParameters.groupId = groupId; + config.queryParameters.orderby = ArtifactSortBy.Name; config.queryParameters.order = SortOrder.Desc; }); diff --git a/app/src/test/java/io/apicurio/registry/storage/impl/readonly/ReadOnlyRegistryStorageTest.java b/app/src/test/java/io/apicurio/registry/storage/impl/readonly/ReadOnlyRegistryStorageTest.java index d6d0d3b05b..66bd18a105 100644 --- a/app/src/test/java/io/apicurio/registry/storage/impl/readonly/ReadOnlyRegistryStorageTest.java +++ b/app/src/test/java/io/apicurio/registry/storage/impl/readonly/ReadOnlyRegistryStorageTest.java @@ -128,7 +128,7 @@ public class ReadOnlyRegistryStorageTest { entry("resolveReferences1", new State(false, s -> s.resolveReferences(null))), entry("searchArtifacts5", new State(false, s -> s.searchArtifacts(null, null, null, 0, 0))), entry("searchGroups5", new State(false, s -> s.searchGroups(null, null, null, null, null))), - entry("searchVersions4", new State(false, s -> s.searchVersions(null, null, 0, 0))), + entry("searchVersions6", new State(false, s -> s.searchVersions(null, null, null, null, 0, 0))), entry("setConfigProperty1", new State(true, s -> { var dto = new DynamicConfigPropertyDto(); dto.setName("test"); diff --git a/common/src/main/resources/META-INF/openapi.json b/common/src/main/resources/META-INF/openapi.json index 963c0547f3..ece1a877fc 100644 --- a/common/src/main/resources/META-INF/openapi.json +++ b/common/src/main/resources/META-INF/openapi.json @@ -311,7 +311,7 @@ "name": "orderby", "description": "The field to sort by. Can be one of:\n\n* `name`\n* `createdOn`\n", "schema": { - "$ref": "#/components/schemas/SortBy" + "$ref": "#/components/schemas/ArtifactSortBy" }, "in": "query" }, @@ -335,7 +335,7 @@ "in": "query" }, { - "name": "group", + "name": "groupId", "description": "Filter by artifact group.", "schema": { "type": "string" @@ -360,6 +360,14 @@ }, "in": "query", "required": false + }, + { + "name": "artifactId", + "description": "Filter by artifactId.", + "schema": { + "type": "string" + }, + "in": "query" } ], "responses": { @@ -438,11 +446,7 @@ "name": "order", "description": "Sort order, ascending (`asc`) or descending (`desc`).", "schema": { - "enum": [ - "asc", - "desc" - ], - "type": "string" + "$ref": "#/components/schemas/SortOrder" }, "in": "query" }, @@ -450,11 +454,7 @@ "name": "orderby", "description": "The field to sort by. Can be one of:\n\n* `name`\n* `createdOn`\n", "schema": { - "enum": [ - "name", - "createdOn" - ], - "type": "string" + "$ref": "#/components/schemas/ArtifactSortBy" }, "in": "query" } @@ -1355,7 +1355,7 @@ "name": "orderby", "description": "The field to sort by. Can be one of:\n\n* `name`\n* `createdOn`\n", "schema": { - "$ref": "#/components/schemas/SortBy" + "$ref": "#/components/schemas/ArtifactSortBy" }, "in": "query" } @@ -1556,7 +1556,7 @@ "name": "orderby", "description": "The field to sort by. Can be one of:\n\n* `name`\n* `createdOn`\n", "schema": { - "$ref": "#/components/schemas/SortBy" + "$ref": "#/components/schemas/GroupSortBy" }, "in": "query" } @@ -1891,6 +1891,22 @@ }, "in": "query", "required": false + }, + { + "name": "order", + "description": "Sort order, ascending (`asc`) or descending (`desc`).", + "schema": { + "$ref": "#/components/schemas/SortOrder" + }, + "in": "query" + }, + { + "name": "orderby", + "description": "The field to sort by. Can be one of:\n\n* `name`\n* `version`\n* `createdOn`\n", + "schema": { + "$ref": "#/components/schemas/VersionSortBy" + }, + "in": "query" } ], "responses": { @@ -1940,16 +1956,6 @@ "tags": [ "Versions" ], - "parameters": [ - { - "name": "ifExists", - "description": "", - "schema": { - "$ref": "#/components/schemas/IfVersionExists" - }, - "in": "query" - } - ], "responses": { "200": { "content": { @@ -2750,6 +2756,98 @@ } ] }, + "/search/groups": { + "summary": "Search for groups in the registry.", + "get": { + "tags": [ + "Search", + "Groups" + ], + "parameters": [ + { + "name": "offset", + "description": "The number of artifacts to skip before starting to collect the result set. Defaults to 0.", + "schema": { + "default": 0, + "type": "integer" + }, + "in": "query", + "required": false + }, + { + "name": "limit", + "description": "The number of artifacts to return. Defaults to 20.", + "schema": { + "default": 20, + "type": "integer" + }, + "in": "query", + "required": false + }, + { + "name": "order", + "description": "Sort order, ascending (`asc`) or descending (`desc`).", + "schema": { + "$ref": "#/components/schemas/SortOrder" + }, + "in": "query" + }, + { + "name": "orderby", + "description": "The field to sort by. Can be one of:\n\n* `name`\n* `createdOn`\n", + "schema": { + "$ref": "#/components/schemas/GroupSortBy" + }, + "in": "query" + }, + { + "name": "labels", + "description": "Filter by one or more name/value label. Separate each name/value pair using a colon. For\nexample `labels=foo:bar` will return only artifacts with a label named `foo`\nand value `bar`.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query" + }, + { + "name": "description", + "description": "Filter by description.", + "schema": { + "type": "string" + }, + "in": "query" + }, + { + "name": "groupId", + "description": "Filter by group name.", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupSearchResults" + } + } + }, + "description": "On a successful response, returns a result set of groups - one for each group\nin the registry that matches the criteria." + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "searchGroups", + "summary": "Search for artifacts", + "description": "Returns a paginated list of all artifacts that match the provided filter criteria.\n" + } + }, "x-codegen-contextRoot": "/apis/registry/v3" }, "components": { @@ -2907,11 +3005,14 @@ ], "type": "string" }, - "SortBy": { + "ArtifactSortBy": { "description": "", "enum": [ - "name", - "createdOn" + "artifactId", + "createdOn", + "modifiedOn", + "artifactType", + "name" ], "type": "string" }, @@ -3312,10 +3413,6 @@ "description": "", "type": "string" }, - "properties": { - "$ref": "#/components/schemas/Labels", - "description": "" - }, "contentId": { "format": "int64", "description": "", @@ -3337,8 +3434,8 @@ "createdOn": "2018-02-10T09:30Z", "owner": "some text", "globalId": 37, - "version": 85, - "properties": {}, + "version": "1.0.7", + "labels": {}, "contentId": 62, "references": {} } @@ -3665,7 +3762,7 @@ "title": "Root Type for CreateGroupMetaData", "description": "", "required": [ - "id" + "groupId" ], "type": "object", "properties": { @@ -3676,13 +3773,13 @@ "$ref": "#/components/schemas/Labels", "description": "" }, - "id": { - "description": "", - "type": "string" + "groupId": { + "$ref": "#/components/schemas/GroupId", + "description": "" } }, "example": { - "id": "group-identifier", + "groupId": "group-identifier", "description": "The description of the artifact.", "labels": { "custom-1": "foo", @@ -4305,12 +4402,22 @@ } } }, - "IfVersionExists": { + "VersionSortBy": { "description": "", "enum": [ - "FAIL", - "CREATE", - "FIND_OR_CREATE" + "version", + "name", + "createdOn", + "modifiedOn", + "globalId" + ], + "type": "string" + }, + "GroupSortBy": { + "description": "", + "enum": [ + "groupId", + "createdOn" ], "type": "string" } diff --git a/go-sdk/pkg/registryclient-v3/groups/groups_request_builder.go b/go-sdk/pkg/registryclient-v3/groups/groups_request_builder.go index 2a94377961..36a24837e9 100644 --- a/go-sdk/pkg/registryclient-v3/groups/groups_request_builder.go +++ b/go-sdk/pkg/registryclient-v3/groups/groups_request_builder.go @@ -23,10 +23,10 @@ type GroupsRequestBuilderGetQueryParameters struct { // Sort order, ascending (`asc`) or descending (`desc`). OrderAsSortOrder *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.SortOrder `uriparametername:"order"` // The field to sort by. Can be one of:* `name`* `createdOn` - // Deprecated: This property is deprecated, use orderbyAsSortBy instead + // Deprecated: This property is deprecated, use orderbyAsGroupSortBy instead Orderby *string `uriparametername:"orderby"` // The field to sort by. Can be one of:* `name`* `createdOn` - OrderbyAsSortBy *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.SortBy `uriparametername:"orderby"` + OrderbyAsGroupSortBy *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.GroupSortBy `uriparametername:"orderby"` } // GroupsRequestBuilderGetRequestConfiguration configuration for the request such as headers, query parameters, and middleware options. diff --git a/go-sdk/pkg/registryclient-v3/groups/item_artifacts_item_versions_request_builder.go b/go-sdk/pkg/registryclient-v3/groups/item_artifacts_item_versions_request_builder.go index 26ff6acd9f..259f118fb2 100644 --- a/go-sdk/pkg/registryclient-v3/groups/item_artifacts_item_versions_request_builder.go +++ b/go-sdk/pkg/registryclient-v3/groups/item_artifacts_item_versions_request_builder.go @@ -17,6 +17,16 @@ type ItemArtifactsItemVersionsRequestBuilderGetQueryParameters struct { Limit *int32 `uriparametername:"limit"` // The number of versions to skip before starting to collect the result set. Defaults to 0. Offset *int32 `uriparametername:"offset"` + // Sort order, ascending (`asc`) or descending (`desc`). + // Deprecated: This property is deprecated, use orderAsSortOrder instead + Order *string `uriparametername:"order"` + // Sort order, ascending (`asc`) or descending (`desc`). + OrderAsSortOrder *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.SortOrder `uriparametername:"order"` + // The field to sort by. Can be one of:* `name`* `version`* `createdOn` + // Deprecated: This property is deprecated, use orderbyAsVersionSortBy instead + Orderby *string `uriparametername:"orderby"` + // The field to sort by. Can be one of:* `name`* `version`* `createdOn` + OrderbyAsVersionSortBy *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.VersionSortBy `uriparametername:"orderby"` } // ItemArtifactsItemVersionsRequestBuilderGetRequestConfiguration configuration for the request such as headers, query parameters, and middleware options. @@ -29,23 +39,12 @@ type ItemArtifactsItemVersionsRequestBuilderGetRequestConfiguration struct { QueryParameters *ItemArtifactsItemVersionsRequestBuilderGetQueryParameters } -// ItemArtifactsItemVersionsRequestBuilderPostQueryParameters creates a new version of the artifact by uploading new content. The configured rules forthe artifact are applied, and if they all pass, the new content is added as the most recent version of the artifact. If any of the rules fail, an error is returned.The body of the request can be the raw content of the new artifact version, or the raw content and a set of references pointing to other artifacts, and the typeof that content should match the artifact's type (for example if the artifact type is `AVRO`then the content of the request should be an Apache Avro document).This operation can fail for the following reasons:* Provided content (request body) was empty (HTTP error `400`)* No artifact with this `artifactId` exists (HTTP error `404`)* The new content violates one of the rules configured for the artifact (HTTP error `409`)* A server error occurred (HTTP error `500`) -type ItemArtifactsItemVersionsRequestBuilderPostQueryParameters struct { - // - // Deprecated: This property is deprecated, use ifExistsAsIfVersionExists instead - IfExists *string `uriparametername:"ifExists"` - // - IfExistsAsIfVersionExists *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.IfVersionExists `uriparametername:"ifExists"` -} - // ItemArtifactsItemVersionsRequestBuilderPostRequestConfiguration configuration for the request such as headers, query parameters, and middleware options. type ItemArtifactsItemVersionsRequestBuilderPostRequestConfiguration struct { // Request headers Headers *i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.RequestHeaders // Request options Options []i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.RequestOption - // Request query parameters - QueryParameters *ItemArtifactsItemVersionsRequestBuilderPostQueryParameters } // ByVersionExpression manage a single version of a single artifact in the registry. @@ -63,7 +62,7 @@ func (m *ItemArtifactsItemVersionsRequestBuilder) ByVersionExpression(versionExp // NewItemArtifactsItemVersionsRequestBuilderInternal instantiates a new VersionsRequestBuilder and sets the default values. func NewItemArtifactsItemVersionsRequestBuilderInternal(pathParameters map[string]string, requestAdapter i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.RequestAdapter) *ItemArtifactsItemVersionsRequestBuilder { m := &ItemArtifactsItemVersionsRequestBuilder{ - BaseRequestBuilder: *i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.NewBaseRequestBuilder(requestAdapter, "{+baseurl}/groups/{groupId}/artifacts/{artifactId}/versions{?offset*,limit*,ifExists*}", pathParameters), + BaseRequestBuilder: *i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.NewBaseRequestBuilder(requestAdapter, "{+baseurl}/groups/{groupId}/artifacts/{artifactId}/versions{?offset*,limit*,order*,orderby*}", pathParameters), } return m } @@ -134,9 +133,6 @@ func (m *ItemArtifactsItemVersionsRequestBuilder) ToGetRequestInformation(ctx co func (m *ItemArtifactsItemVersionsRequestBuilder) ToPostRequestInformation(ctx context.Context, body i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.CreateVersionable, requestConfiguration *ItemArtifactsItemVersionsRequestBuilderPostRequestConfiguration) (*i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.RequestInformation, error) { requestInfo := i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.NewRequestInformationWithMethodAndUrlTemplateAndPathParameters(i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.POST, m.BaseRequestBuilder.UrlTemplate, m.BaseRequestBuilder.PathParameters) if requestConfiguration != nil { - if requestConfiguration.QueryParameters != nil { - requestInfo.AddQueryParameters(*(requestConfiguration.QueryParameters)) - } requestInfo.Headers.AddAll(requestConfiguration.Headers) requestInfo.AddRequestOptions(requestConfiguration.Options) } diff --git a/go-sdk/pkg/registryclient-v3/groups/item_artifacts_request_builder.go b/go-sdk/pkg/registryclient-v3/groups/item_artifacts_request_builder.go index 1e819265d9..0580256fbe 100644 --- a/go-sdk/pkg/registryclient-v3/groups/item_artifacts_request_builder.go +++ b/go-sdk/pkg/registryclient-v3/groups/item_artifacts_request_builder.go @@ -31,10 +31,10 @@ type ItemArtifactsRequestBuilderGetQueryParameters struct { // Sort order, ascending (`asc`) or descending (`desc`). OrderAsSortOrder *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.SortOrder `uriparametername:"order"` // The field to sort by. Can be one of:* `name`* `createdOn` - // Deprecated: This property is deprecated, use orderbyAsSortBy instead + // Deprecated: This property is deprecated, use orderbyAsArtifactSortBy instead Orderby *string `uriparametername:"orderby"` // The field to sort by. Can be one of:* `name`* `createdOn` - OrderbyAsSortBy *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.SortBy `uriparametername:"orderby"` + OrderbyAsArtifactSortBy *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.ArtifactSortBy `uriparametername:"orderby"` } // ItemArtifactsRequestBuilderGetRequestConfiguration configuration for the request such as headers, query parameters, and middleware options. diff --git a/go-sdk/pkg/registryclient-v3/kiota-lock.json b/go-sdk/pkg/registryclient-v3/kiota-lock.json index dc284cdc83..9ea3d354ba 100644 --- a/go-sdk/pkg/registryclient-v3/kiota-lock.json +++ b/go-sdk/pkg/registryclient-v3/kiota-lock.json @@ -1,5 +1,5 @@ { - "descriptionHash": "33B46297DBD2EE0DBD2198607D4A7C9E9FC2392087FC31BB26122B143524742D6FA5A1D768BCB6CD4AC8613E069E7B4284FC12954C47905CE723AE294DC2FC0C", + "descriptionHash": "63866A1698DBB7DE89156A5B48D7BDCEC3C8CA3C96D8789E209496ABED828D4388775AEA55B710450C15EFCC7C99C60D43395DE7648C86CAB639E3683E00B82C", "descriptionLocation": "../../v3.json", "lockFileVersion": "1.0.0", "kiotaVersion": "1.10.1", diff --git a/go-sdk/pkg/registryclient-v3/models/artifact_sort_by.go b/go-sdk/pkg/registryclient-v3/models/artifact_sort_by.go new file mode 100644 index 0000000000..8096a92771 --- /dev/null +++ b/go-sdk/pkg/registryclient-v3/models/artifact_sort_by.go @@ -0,0 +1,47 @@ +package models + +import ( + "errors" +) + +type ArtifactSortBy int + +const ( + ARTIFACTID_ARTIFACTSORTBY ArtifactSortBy = iota + CREATEDON_ARTIFACTSORTBY + MODIFIEDON_ARTIFACTSORTBY + ARTIFACTTYPE_ARTIFACTSORTBY + NAME_ARTIFACTSORTBY +) + +func (i ArtifactSortBy) String() string { + return []string{"artifactId", "createdOn", "modifiedOn", "artifactType", "name"}[i] +} +func ParseArtifactSortBy(v string) (any, error) { + result := ARTIFACTID_ARTIFACTSORTBY + switch v { + case "artifactId": + result = ARTIFACTID_ARTIFACTSORTBY + case "createdOn": + result = CREATEDON_ARTIFACTSORTBY + case "modifiedOn": + result = MODIFIEDON_ARTIFACTSORTBY + case "artifactType": + result = ARTIFACTTYPE_ARTIFACTSORTBY + case "name": + result = NAME_ARTIFACTSORTBY + default: + return 0, errors.New("Unknown ArtifactSortBy value: " + v) + } + return &result, nil +} +func SerializeArtifactSortBy(values []ArtifactSortBy) []string { + result := make([]string, len(values)) + for i, v := range values { + result[i] = v.String() + } + return result +} +func (i ArtifactSortBy) isMultiValue() bool { + return false +} diff --git a/go-sdk/pkg/registryclient-v3/models/create_group.go b/go-sdk/pkg/registryclient-v3/models/create_group.go index ea3136fd97..a12b4df4c4 100644 --- a/go-sdk/pkg/registryclient-v3/models/create_group.go +++ b/go-sdk/pkg/registryclient-v3/models/create_group.go @@ -10,8 +10,8 @@ type CreateGroup struct { additionalData map[string]any // The description property description *string - // The id property - id *string + // An ID of a single artifact group. + groupId *string // User-defined name-value pairs. Name and value must be strings. labels Labelsable } @@ -51,13 +51,13 @@ func (m *CreateGroup) GetFieldDeserializers() map[string]func(i878a80d2330e89d26 } return nil } - res["id"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { + res["groupId"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { val, err := n.GetStringValue() if err != nil { return err } if val != nil { - m.SetId(val) + m.SetGroupId(val) } return nil } @@ -74,9 +74,9 @@ func (m *CreateGroup) GetFieldDeserializers() map[string]func(i878a80d2330e89d26 return res } -// GetId gets the id property value. The id property -func (m *CreateGroup) GetId() *string { - return m.id +// GetGroupId gets the groupId property value. An ID of a single artifact group. +func (m *CreateGroup) GetGroupId() *string { + return m.groupId } // GetLabels gets the labels property value. User-defined name-value pairs. Name and value must be strings. @@ -93,7 +93,7 @@ func (m *CreateGroup) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0a0e6 } } { - err := writer.WriteStringValue("id", m.GetId()) + err := writer.WriteStringValue("groupId", m.GetGroupId()) if err != nil { return err } @@ -123,9 +123,9 @@ func (m *CreateGroup) SetDescription(value *string) { m.description = value } -// SetId sets the id property value. The id property -func (m *CreateGroup) SetId(value *string) { - m.id = value +// SetGroupId sets the groupId property value. An ID of a single artifact group. +func (m *CreateGroup) SetGroupId(value *string) { + m.groupId = value } // SetLabels sets the labels property value. User-defined name-value pairs. Name and value must be strings. @@ -138,9 +138,9 @@ type CreateGroupable interface { i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.AdditionalDataHolder i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable GetDescription() *string - GetId() *string + GetGroupId() *string GetLabels() Labelsable SetDescription(value *string) - SetId(value *string) + SetGroupId(value *string) SetLabels(value Labelsable) } diff --git a/go-sdk/pkg/registryclient-v3/models/group_sort_by.go b/go-sdk/pkg/registryclient-v3/models/group_sort_by.go new file mode 100644 index 0000000000..9040ff1dde --- /dev/null +++ b/go-sdk/pkg/registryclient-v3/models/group_sort_by.go @@ -0,0 +1,38 @@ +package models + +import ( + "errors" +) + +type GroupSortBy int + +const ( + GROUPID_GROUPSORTBY GroupSortBy = iota + CREATEDON_GROUPSORTBY +) + +func (i GroupSortBy) String() string { + return []string{"groupId", "createdOn"}[i] +} +func ParseGroupSortBy(v string) (any, error) { + result := GROUPID_GROUPSORTBY + switch v { + case "groupId": + result = GROUPID_GROUPSORTBY + case "createdOn": + result = CREATEDON_GROUPSORTBY + default: + return 0, errors.New("Unknown GroupSortBy value: " + v) + } + return &result, nil +} +func SerializeGroupSortBy(values []GroupSortBy) []string { + result := make([]string, len(values)) + for i, v := range values { + result[i] = v.String() + } + return result +} +func (i GroupSortBy) isMultiValue() bool { + return false +} diff --git a/go-sdk/pkg/registryclient-v3/models/if_version_exists.go b/go-sdk/pkg/registryclient-v3/models/if_version_exists.go deleted file mode 100644 index 3a6901f318..0000000000 --- a/go-sdk/pkg/registryclient-v3/models/if_version_exists.go +++ /dev/null @@ -1,41 +0,0 @@ -package models - -import ( - "errors" -) - -type IfVersionExists int - -const ( - FAIL_IFVERSIONEXISTS IfVersionExists = iota - CREATE_IFVERSIONEXISTS - FIND_OR_CREATE_IFVERSIONEXISTS -) - -func (i IfVersionExists) String() string { - return []string{"FAIL", "CREATE", "FIND_OR_CREATE"}[i] -} -func ParseIfVersionExists(v string) (any, error) { - result := FAIL_IFVERSIONEXISTS - switch v { - case "FAIL": - result = FAIL_IFVERSIONEXISTS - case "CREATE": - result = CREATE_IFVERSIONEXISTS - case "FIND_OR_CREATE": - result = FIND_OR_CREATE_IFVERSIONEXISTS - default: - return 0, errors.New("Unknown IfVersionExists value: " + v) - } - return &result, nil -} -func SerializeIfVersionExists(values []IfVersionExists) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.String() - } - return result -} -func (i IfVersionExists) isMultiValue() bool { - return false -} diff --git a/go-sdk/pkg/registryclient-v3/models/searched_version.go b/go-sdk/pkg/registryclient-v3/models/searched_version.go index b7bfacd68b..911cb08184 100644 --- a/go-sdk/pkg/registryclient-v3/models/searched_version.go +++ b/go-sdk/pkg/registryclient-v3/models/searched_version.go @@ -21,8 +21,6 @@ type SearchedVersion struct { name *string // The owner property owner *string - // User-defined name-value pairs. Name and value must be strings. - properties Labelsable // The references property references []ArtifactReferenceable // Describes the state of an artifact or artifact version. The following statesare possible:* ENABLED* DISABLED* DEPRECATED @@ -128,16 +126,6 @@ func (m *SearchedVersion) GetFieldDeserializers() map[string]func(i878a80d2330e8 } return nil } - res["properties"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { - val, err := n.GetObjectValue(CreateLabelsFromDiscriminatorValue) - if err != nil { - return err - } - if val != nil { - m.SetProperties(val.(Labelsable)) - } - return nil - } res["references"] = func(n i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.ParseNode) error { val, err := n.GetCollectionOfObjectValues(CreateArtifactReferenceFromDiscriminatorValue) if err != nil { @@ -202,11 +190,6 @@ func (m *SearchedVersion) GetOwner() *string { return m.owner } -// GetProperties gets the properties property value. User-defined name-value pairs. Name and value must be strings. -func (m *SearchedVersion) GetProperties() Labelsable { - return m.properties -} - // GetReferences gets the references property value. The references property func (m *SearchedVersion) GetReferences() []ArtifactReferenceable { return m.references @@ -265,12 +248,6 @@ func (m *SearchedVersion) Serialize(writer i878a80d2330e89d26896388a3f487eef27b0 return err } } - { - err := writer.WriteObjectValue("properties", m.GetProperties()) - if err != nil { - return err - } - } if m.GetReferences() != nil { cast := make([]i878a80d2330e89d26896388a3f487eef27b0a0e6c010c493bf80be1452208f91.Parsable, len(m.GetReferences())) for i, v := range m.GetReferences() { @@ -346,11 +323,6 @@ func (m *SearchedVersion) SetOwner(value *string) { m.owner = value } -// SetProperties sets the properties property value. User-defined name-value pairs. Name and value must be strings. -func (m *SearchedVersion) SetProperties(value Labelsable) { - m.properties = value -} - // SetReferences sets the references property value. The references property func (m *SearchedVersion) SetReferences(value []ArtifactReferenceable) { m.references = value @@ -381,7 +353,6 @@ type SearchedVersionable interface { GetGlobalId() *int64 GetName() *string GetOwner() *string - GetProperties() Labelsable GetReferences() []ArtifactReferenceable GetState() *VersionState GetTypeEscaped() *string @@ -392,7 +363,6 @@ type SearchedVersionable interface { SetGlobalId(value *int64) SetName(value *string) SetOwner(value *string) - SetProperties(value Labelsable) SetReferences(value []ArtifactReferenceable) SetState(value *VersionState) SetTypeEscaped(value *string) diff --git a/go-sdk/pkg/registryclient-v3/models/sort_by.go b/go-sdk/pkg/registryclient-v3/models/sort_by.go deleted file mode 100644 index dd811bd895..0000000000 --- a/go-sdk/pkg/registryclient-v3/models/sort_by.go +++ /dev/null @@ -1,38 +0,0 @@ -package models - -import ( - "errors" -) - -type SortBy int - -const ( - NAME_SORTBY SortBy = iota - CREATEDON_SORTBY -) - -func (i SortBy) String() string { - return []string{"name", "createdOn"}[i] -} -func ParseSortBy(v string) (any, error) { - result := NAME_SORTBY - switch v { - case "name": - result = NAME_SORTBY - case "createdOn": - result = CREATEDON_SORTBY - default: - return 0, errors.New("Unknown SortBy value: " + v) - } - return &result, nil -} -func SerializeSortBy(values []SortBy) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.String() - } - return result -} -func (i SortBy) isMultiValue() bool { - return false -} diff --git a/go-sdk/pkg/registryclient-v3/models/version_sort_by.go b/go-sdk/pkg/registryclient-v3/models/version_sort_by.go new file mode 100644 index 0000000000..5ae6490478 --- /dev/null +++ b/go-sdk/pkg/registryclient-v3/models/version_sort_by.go @@ -0,0 +1,47 @@ +package models + +import ( + "errors" +) + +type VersionSortBy int + +const ( + VERSION_VERSIONSORTBY VersionSortBy = iota + NAME_VERSIONSORTBY + CREATEDON_VERSIONSORTBY + MODIFIEDON_VERSIONSORTBY + GLOBALID_VERSIONSORTBY +) + +func (i VersionSortBy) String() string { + return []string{"version", "name", "createdOn", "modifiedOn", "globalId"}[i] +} +func ParseVersionSortBy(v string) (any, error) { + result := VERSION_VERSIONSORTBY + switch v { + case "version": + result = VERSION_VERSIONSORTBY + case "name": + result = NAME_VERSIONSORTBY + case "createdOn": + result = CREATEDON_VERSIONSORTBY + case "modifiedOn": + result = MODIFIEDON_VERSIONSORTBY + case "globalId": + result = GLOBALID_VERSIONSORTBY + default: + return 0, errors.New("Unknown VersionSortBy value: " + v) + } + return &result, nil +} +func SerializeVersionSortBy(values []VersionSortBy) []string { + result := make([]string, len(values)) + for i, v := range values { + result[i] = v.String() + } + return result +} +func (i VersionSortBy) isMultiValue() bool { + return false +} diff --git a/go-sdk/pkg/registryclient-v3/search/artifacts/post_order_query_parameter_type.go b/go-sdk/pkg/registryclient-v3/search/artifacts/post_order_query_parameter_type.go deleted file mode 100644 index ce5429c705..0000000000 --- a/go-sdk/pkg/registryclient-v3/search/artifacts/post_order_query_parameter_type.go +++ /dev/null @@ -1,39 +0,0 @@ -package artifacts - -import ( - "errors" -) - -// Search for artifacts in the registry. -type PostOrderQueryParameterType int - -const ( - ASC_POSTORDERQUERYPARAMETERTYPE PostOrderQueryParameterType = iota - DESC_POSTORDERQUERYPARAMETERTYPE -) - -func (i PostOrderQueryParameterType) String() string { - return []string{"asc", "desc"}[i] -} -func ParsePostOrderQueryParameterType(v string) (any, error) { - result := ASC_POSTORDERQUERYPARAMETERTYPE - switch v { - case "asc": - result = ASC_POSTORDERQUERYPARAMETERTYPE - case "desc": - result = DESC_POSTORDERQUERYPARAMETERTYPE - default: - return 0, errors.New("Unknown PostOrderQueryParameterType value: " + v) - } - return &result, nil -} -func SerializePostOrderQueryParameterType(values []PostOrderQueryParameterType) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.String() - } - return result -} -func (i PostOrderQueryParameterType) isMultiValue() bool { - return false -} diff --git a/go-sdk/pkg/registryclient-v3/search/artifacts/post_orderby_query_parameter_type.go b/go-sdk/pkg/registryclient-v3/search/artifacts/post_orderby_query_parameter_type.go deleted file mode 100644 index 1c409fc16e..0000000000 --- a/go-sdk/pkg/registryclient-v3/search/artifacts/post_orderby_query_parameter_type.go +++ /dev/null @@ -1,39 +0,0 @@ -package artifacts - -import ( - "errors" -) - -// Search for artifacts in the registry. -type PostOrderbyQueryParameterType int - -const ( - NAME_POSTORDERBYQUERYPARAMETERTYPE PostOrderbyQueryParameterType = iota - CREATEDON_POSTORDERBYQUERYPARAMETERTYPE -) - -func (i PostOrderbyQueryParameterType) String() string { - return []string{"name", "createdOn"}[i] -} -func ParsePostOrderbyQueryParameterType(v string) (any, error) { - result := NAME_POSTORDERBYQUERYPARAMETERTYPE - switch v { - case "name": - result = NAME_POSTORDERBYQUERYPARAMETERTYPE - case "createdOn": - result = CREATEDON_POSTORDERBYQUERYPARAMETERTYPE - default: - return 0, errors.New("Unknown PostOrderbyQueryParameterType value: " + v) - } - return &result, nil -} -func SerializePostOrderbyQueryParameterType(values []PostOrderbyQueryParameterType) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.String() - } - return result -} -func (i PostOrderbyQueryParameterType) isMultiValue() bool { - return false -} diff --git a/go-sdk/pkg/registryclient-v3/search/artifacts_request_builder.go b/go-sdk/pkg/registryclient-v3/search/artifacts_request_builder.go index 074952173f..14b36d5c71 100644 --- a/go-sdk/pkg/registryclient-v3/search/artifacts_request_builder.go +++ b/go-sdk/pkg/registryclient-v3/search/artifacts_request_builder.go @@ -3,7 +3,6 @@ package search import ( "context" i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71 "github.com/apicurio/apicurio-registry/go-sdk/pkg/registryclient-v3/models" - i3e398dcce307917e6a6621007d111590e40d6b2dbc08b28d47e9323689debc39 "github.com/apicurio/apicurio-registry/go-sdk/pkg/registryclient-v3/search/artifacts" i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f "github.com/microsoft/kiota-abstractions-go" ) @@ -14,6 +13,8 @@ type ArtifactsRequestBuilder struct { // ArtifactsRequestBuilderGetQueryParameters returns a paginated list of all artifacts that match the provided filter criteria. type ArtifactsRequestBuilderGetQueryParameters struct { + // Filter by artifactId. + ArtifactId *string `uriparametername:"artifactId"` // Filter by contentId. ContentId *int64 `uriparametername:"contentId"` // Filter by description. @@ -21,7 +22,7 @@ type ArtifactsRequestBuilderGetQueryParameters struct { // Filter by globalId. GlobalId *int64 `uriparametername:"globalId"` // Filter by artifact group. - Group *string `uriparametername:"group"` + GroupId *string `uriparametername:"groupId"` // Filter by one or more name/value label. Separate each name/value pair using a colon. Forexample `labels=foo:bar` will return only artifacts with a label named `foo`and value `bar`. Labels []string `uriparametername:"labels"` // The number of artifacts to return. Defaults to 20. @@ -36,10 +37,10 @@ type ArtifactsRequestBuilderGetQueryParameters struct { // Sort order, ascending (`asc`) or descending (`desc`). OrderAsSortOrder *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.SortOrder `uriparametername:"order"` // The field to sort by. Can be one of:* `name`* `createdOn` - // Deprecated: This property is deprecated, use orderbyAsSortBy instead + // Deprecated: This property is deprecated, use orderbyAsArtifactSortBy instead Orderby *string `uriparametername:"orderby"` // The field to sort by. Can be one of:* `name`* `createdOn` - OrderbyAsSortBy *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.SortBy `uriparametername:"orderby"` + OrderbyAsArtifactSortBy *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.ArtifactSortBy `uriparametername:"orderby"` } // ArtifactsRequestBuilderGetRequestConfiguration configuration for the request such as headers, query parameters, and middleware options. @@ -63,15 +64,15 @@ type ArtifactsRequestBuilderPostQueryParameters struct { // The number of artifacts to skip before starting to collect the result set. Defaults to 0. Offset *int32 `uriparametername:"offset"` // Sort order, ascending (`asc`) or descending (`desc`). - // Deprecated: This property is deprecated, use orderAsPostOrderQueryParameterType instead + // Deprecated: This property is deprecated, use orderAsSortOrder instead Order *string `uriparametername:"order"` // Sort order, ascending (`asc`) or descending (`desc`). - OrderAsPostOrderQueryParameterType *i3e398dcce307917e6a6621007d111590e40d6b2dbc08b28d47e9323689debc39.PostOrderQueryParameterType `uriparametername:"order"` + OrderAsSortOrder *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.SortOrder `uriparametername:"order"` // The field to sort by. Can be one of:* `name`* `createdOn` - // Deprecated: This property is deprecated, use orderbyAsPostOrderbyQueryParameterType instead + // Deprecated: This property is deprecated, use orderbyAsArtifactSortBy instead Orderby *string `uriparametername:"orderby"` // The field to sort by. Can be one of:* `name`* `createdOn` - OrderbyAsPostOrderbyQueryParameterType *i3e398dcce307917e6a6621007d111590e40d6b2dbc08b28d47e9323689debc39.PostOrderbyQueryParameterType `uriparametername:"orderby"` + OrderbyAsArtifactSortBy *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.ArtifactSortBy `uriparametername:"orderby"` } // ArtifactsRequestBuilderPostRequestConfiguration configuration for the request such as headers, query parameters, and middleware options. @@ -87,7 +88,7 @@ type ArtifactsRequestBuilderPostRequestConfiguration struct { // NewArtifactsRequestBuilderInternal instantiates a new ArtifactsRequestBuilder and sets the default values. func NewArtifactsRequestBuilderInternal(pathParameters map[string]string, requestAdapter i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.RequestAdapter) *ArtifactsRequestBuilder { m := &ArtifactsRequestBuilder{ - BaseRequestBuilder: *i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.NewBaseRequestBuilder(requestAdapter, "{+baseurl}/search/artifacts{?name*,offset*,limit*,order*,orderby*,labels*,description*,group*,globalId*,contentId*,canonical*,artifactType*}", pathParameters), + BaseRequestBuilder: *i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.NewBaseRequestBuilder(requestAdapter, "{+baseurl}/search/artifacts{?name*,offset*,limit*,order*,orderby*,labels*,description*,groupId*,globalId*,contentId*,artifactId*,canonical*,artifactType*}", pathParameters), } return m } diff --git a/go-sdk/pkg/registryclient-v3/search/groups_request_builder.go b/go-sdk/pkg/registryclient-v3/search/groups_request_builder.go new file mode 100644 index 0000000000..7b6fcc81ce --- /dev/null +++ b/go-sdk/pkg/registryclient-v3/search/groups_request_builder.go @@ -0,0 +1,99 @@ +package search + +import ( + "context" + i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71 "github.com/apicurio/apicurio-registry/go-sdk/pkg/registryclient-v3/models" + i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f "github.com/microsoft/kiota-abstractions-go" +) + +// GroupsRequestBuilder search for groups in the registry. +type GroupsRequestBuilder struct { + i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.BaseRequestBuilder +} + +// GroupsRequestBuilderGetQueryParameters returns a paginated list of all artifacts that match the provided filter criteria. +type GroupsRequestBuilderGetQueryParameters struct { + // Filter by description. + Description *string `uriparametername:"description"` + // Filter by group name. + GroupId *string `uriparametername:"groupId"` + // Filter by one or more name/value label. Separate each name/value pair using a colon. Forexample `labels=foo:bar` will return only artifacts with a label named `foo`and value `bar`. + Labels []string `uriparametername:"labels"` + // The number of artifacts to return. Defaults to 20. + Limit *int32 `uriparametername:"limit"` + // The number of artifacts to skip before starting to collect the result set. Defaults to 0. + Offset *int32 `uriparametername:"offset"` + // Sort order, ascending (`asc`) or descending (`desc`). + // Deprecated: This property is deprecated, use orderAsSortOrder instead + Order *string `uriparametername:"order"` + // Sort order, ascending (`asc`) or descending (`desc`). + OrderAsSortOrder *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.SortOrder `uriparametername:"order"` + // The field to sort by. Can be one of:* `name`* `createdOn` + // Deprecated: This property is deprecated, use orderbyAsGroupSortBy instead + Orderby *string `uriparametername:"orderby"` + // The field to sort by. Can be one of:* `name`* `createdOn` + OrderbyAsGroupSortBy *i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.GroupSortBy `uriparametername:"orderby"` +} + +// GroupsRequestBuilderGetRequestConfiguration configuration for the request such as headers, query parameters, and middleware options. +type GroupsRequestBuilderGetRequestConfiguration struct { + // Request headers + Headers *i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.RequestHeaders + // Request options + Options []i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.RequestOption + // Request query parameters + QueryParameters *GroupsRequestBuilderGetQueryParameters +} + +// NewGroupsRequestBuilderInternal instantiates a new GroupsRequestBuilder and sets the default values. +func NewGroupsRequestBuilderInternal(pathParameters map[string]string, requestAdapter i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.RequestAdapter) *GroupsRequestBuilder { + m := &GroupsRequestBuilder{ + BaseRequestBuilder: *i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.NewBaseRequestBuilder(requestAdapter, "{+baseurl}/search/groups{?offset*,limit*,order*,orderby*,labels*,description*,groupId*}", pathParameters), + } + return m +} + +// NewGroupsRequestBuilder instantiates a new GroupsRequestBuilder and sets the default values. +func NewGroupsRequestBuilder(rawUrl string, requestAdapter i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.RequestAdapter) *GroupsRequestBuilder { + urlParams := make(map[string]string) + urlParams["request-raw-url"] = rawUrl + return NewGroupsRequestBuilderInternal(urlParams, requestAdapter) +} + +// Get returns a paginated list of all artifacts that match the provided filter criteria. +func (m *GroupsRequestBuilder) Get(ctx context.Context, requestConfiguration *GroupsRequestBuilderGetRequestConfiguration) (i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.GroupSearchResultsable, error) { + requestInfo, err := m.ToGetRequestInformation(ctx, requestConfiguration) + if err != nil { + return nil, err + } + errorMapping := i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.ErrorMappings{ + "500": i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.CreateErrorFromDiscriminatorValue, + } + res, err := m.BaseRequestBuilder.RequestAdapter.Send(ctx, requestInfo, i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.CreateGroupSearchResultsFromDiscriminatorValue, errorMapping) + if err != nil { + return nil, err + } + if res == nil { + return nil, nil + } + return res.(i00eb2e63d156923d00d8e86fe16b5d74daf30e363c9f185a8165cb42aa2f2c71.GroupSearchResultsable), nil +} + +// ToGetRequestInformation returns a paginated list of all artifacts that match the provided filter criteria. +func (m *GroupsRequestBuilder) ToGetRequestInformation(ctx context.Context, requestConfiguration *GroupsRequestBuilderGetRequestConfiguration) (*i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.RequestInformation, error) { + requestInfo := i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.NewRequestInformationWithMethodAndUrlTemplateAndPathParameters(i2ae4187f7daee263371cb1c977df639813ab50ffa529013b7437480d1ec0158f.GET, m.BaseRequestBuilder.UrlTemplate, m.BaseRequestBuilder.PathParameters) + if requestConfiguration != nil { + if requestConfiguration.QueryParameters != nil { + requestInfo.AddQueryParameters(*(requestConfiguration.QueryParameters)) + } + requestInfo.Headers.AddAll(requestConfiguration.Headers) + requestInfo.AddRequestOptions(requestConfiguration.Options) + } + requestInfo.Headers.TryAdd("Accept", "application/json") + return requestInfo, nil +} + +// WithUrl returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. +func (m *GroupsRequestBuilder) WithUrl(rawUrl string) *GroupsRequestBuilder { + return NewGroupsRequestBuilder(rawUrl, m.BaseRequestBuilder.RequestAdapter) +} diff --git a/go-sdk/pkg/registryclient-v3/search/search_request_builder.go b/go-sdk/pkg/registryclient-v3/search/search_request_builder.go index 27c0d9f6ca..d6a76626c8 100644 --- a/go-sdk/pkg/registryclient-v3/search/search_request_builder.go +++ b/go-sdk/pkg/registryclient-v3/search/search_request_builder.go @@ -28,3 +28,8 @@ func NewSearchRequestBuilder(rawUrl string, requestAdapter i2ae4187f7daee263371c urlParams["request-raw-url"] = rawUrl return NewSearchRequestBuilderInternal(urlParams, requestAdapter) } + +// Groups search for groups in the registry. +func (m *SearchRequestBuilder) Groups() *GroupsRequestBuilder { + return NewGroupsRequestBuilderInternal(m.BaseRequestBuilder.PathParameters, m.BaseRequestBuilder.RequestAdapter) +} diff --git a/integration-tests/src/test/java/io/apicurio/tests/ApicurioRegistryBaseIT.java b/integration-tests/src/test/java/io/apicurio/tests/ApicurioRegistryBaseIT.java index 182268bc75..d66cc37306 100644 --- a/integration-tests/src/test/java/io/apicurio/tests/ApicurioRegistryBaseIT.java +++ b/integration-tests/src/test/java/io/apicurio/tests/ApicurioRegistryBaseIT.java @@ -135,10 +135,10 @@ private static String normalizeGroupId(String groupId) { } protected CreateArtifactResponse createArtifact(String groupId, String artifactId, String artifactType, String content, - String contentType, IfArtifactExists ifExists, Function customizer) throws Exception { + String contentType, IfArtifactExists ifExists, Consumer customizer) throws Exception { CreateArtifact createArtifact = TestUtils.clientCreateArtifact(artifactId, artifactType, content, contentType); if (customizer != null) { - customizer.apply(createArtifact); + customizer.accept(createArtifact); } var response = registryClient.groups().byGroupId(groupId).artifacts().post(createArtifact, config -> { config.queryParameters.canonical = false; @@ -155,10 +155,10 @@ protected CreateArtifactResponse createArtifact(String groupId, String artifactI return response; } - protected VersionMetaData createArtifactVersion(String groupId, String artifactId, String content, String contentType, Function customizer) throws Exception { + protected VersionMetaData createArtifactVersion(String groupId, String artifactId, String content, String contentType, Consumer customizer) throws Exception { CreateVersion createVersion = TestUtils.clientCreateVersion(content, contentType); if (customizer != null) { - customizer.apply(createVersion); + customizer.accept(createVersion); } VersionMetaData meta = registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions().post(createVersion); diff --git a/integration-tests/src/test/java/io/apicurio/tests/smokeTests/apicurio/ArtifactsIT.java b/integration-tests/src/test/java/io/apicurio/tests/smokeTests/apicurio/ArtifactsIT.java index 5a119b74a4..20ba79b605 100644 --- a/integration-tests/src/test/java/io/apicurio/tests/smokeTests/apicurio/ArtifactsIT.java +++ b/integration-tests/src/test/java/io/apicurio/tests/smokeTests/apicurio/ArtifactsIT.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.apicurio.registry.rest.client.models.ArtifactSearchResults; +import io.apicurio.registry.rest.client.models.ArtifactSortBy; import io.apicurio.registry.rest.client.models.CreateArtifact; import io.apicurio.registry.rest.client.models.CreateArtifactResponse; import io.apicurio.registry.rest.client.models.CreateVersion; @@ -10,7 +11,6 @@ import io.apicurio.registry.rest.client.models.IfArtifactExists; import io.apicurio.registry.rest.client.models.Rule; import io.apicurio.registry.rest.client.models.RuleType; -import io.apicurio.registry.rest.client.models.SortBy; import io.apicurio.registry.rest.client.models.SortOrder; import io.apicurio.registry.rest.client.models.VersionContent; import io.apicurio.registry.rest.client.models.VersionMetaData; @@ -242,7 +242,6 @@ void testVersionAlreadyExistsIfExistsCreateVersion() throws Exception { var caResponse = createArtifact(groupId, artifactId, ArtifactType.AVRO, artifactData, ContentTypes.APPLICATION_JSON, null, (ca) -> { ca.getFirstVersion().setVersion("1.1"); - return null; }); LOGGER.info("Created artifact {} with metadata {}", artifactId, caResponse.getArtifact().toString()); @@ -251,7 +250,6 @@ void testVersionAlreadyExistsIfExistsCreateVersion() throws Exception { assertClientError("VersionAlreadyExistsException", 409, () -> createArtifact(groupId, artifactId, ArtifactType.AVRO, sameArtifactData, ContentTypes.APPLICATION_JSON, IfArtifactExists.CREATE_VERSION, (ca) -> { ca.getFirstVersion().setVersion("1.1"); - return null; }), true, errorCodeExtractor); } @@ -368,7 +366,7 @@ void testAllowedSpecialCharacters() throws Exception { retryOp((rc) -> rc.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).post(vc)); registryClient.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.name = artifactId; config.queryParameters.offset = 0; config.queryParameters.limit = 10; @@ -406,7 +404,7 @@ void testAllowedSpecialCharactersCreateViaApi() throws Exception { retryOp((rc) -> rc.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).post(vc)); registryClient.search().artifacts().get(config -> { - config.queryParameters.group = groupId; + config.queryParameters.groupId = groupId; config.queryParameters.name = artifactId; config.queryParameters.offset = 0; config.queryParameters.limit = 100; @@ -436,9 +434,9 @@ public void testSearchOrderBy() throws Exception { } ArtifactSearchResults results = registryClient.search().artifacts().get(config -> { - config.queryParameters.group = group; + config.queryParameters.groupId = group; config.queryParameters.order = SortOrder.Asc; - config.queryParameters.orderby = SortBy.CreatedOn; + config.queryParameters.orderby = ArtifactSortBy.CreatedOn; config.queryParameters.offset = 0; config.queryParameters.limit = 10; }); diff --git a/integration-tests/src/test/java/io/apicurio/tests/smokeTests/apicurio/MetadataIT.java b/integration-tests/src/test/java/io/apicurio/tests/smokeTests/apicurio/MetadataIT.java index 8349d3cbab..5635604096 100644 --- a/integration-tests/src/test/java/io/apicurio/tests/smokeTests/apicurio/MetadataIT.java +++ b/integration-tests/src/test/java/io/apicurio/tests/smokeTests/apicurio/MetadataIT.java @@ -70,7 +70,6 @@ void getAndUpdateMetadataOfArtifactSpecificVersion() throws Exception { var caResponse = createArtifact(groupId, artifactId, ArtifactType.AVRO, artifactDefinition, ContentTypes.APPLICATION_JSON, null, (ca) -> { ca.getFirstVersion().setName("Version 1 Name"); - return null; }); LOGGER.info("Created artifact {} with metadata {}", artifactId, caResponse.getArtifact()); diff --git a/ui/tests/specs/e2e.spec.ts b/ui/tests/specs/e2e.spec.ts index eabeb02503..66048988b3 100644 --- a/ui/tests/specs/e2e.spec.ts +++ b/ui/tests/specs/e2e.spec.ts @@ -7,45 +7,44 @@ const OPENAPI_DATA_V2_STR: string = JSON.stringify(OPENAPI_DATA_V2, null, 4); const REGISTRY_UI_URL: string = process.env["REGISTRY_UI_URL"] || "http://localhost:8888"; -test("End to End - Upload artifact", async ({ page }) => { +test("End to End - Create artifact", async ({ page }) => { await page.goto(REGISTRY_UI_URL); await expect(page).toHaveTitle(/Apicurio Registry/); - expect(page.getByTestId("btn-toolbar-upload-artifact")).toBeDefined(); + expect(page.getByTestId("btn-toolbar-create-artifact")).toBeDefined(); - // Click the "Upload artifact" button - await page.getByTestId("btn-toolbar-upload-artifact").click(); - await expect(page.getByTestId("upload-artifact-form-group")).toHaveValue(""); + // Click the "Create artifact" button + await page.getByTestId("btn-toolbar-create-artifact").click(); + await expect(page.getByTestId("create-artifact-form-group")).toHaveValue(""); - // Upload a new artifact - await page.getByTestId("upload-artifact-form-group").fill("e2e"); - await page.getByTestId("upload-artifact-form-id").fill("MyArtifact"); - await page.getByTestId("upload-artifact-form-type-select").click(); - await page.getByTestId("upload-artifact-form-OPENAPI").click(); + // Create a new artifact + await page.getByTestId("create-artifact-form-group").fill("e2e"); + await page.getByTestId("create-artifact-form-id").fill("MyArtifact"); + await page.getByTestId("create-artifact-form-type-select").click(); + await page.getByTestId("create-artifact-form-OPENAPI").click(); await page.locator("#artifact-content").fill(OPENAPI_DATA_STR); - await page.getByTestId("upload-artifact-modal-btn-upload").click(); + await page.getByTestId("create-artifact-modal-btn-create").click(); - // Make sure we redirected to the artifact detail page. - await expect(page).toHaveURL(/.+\/artifacts\/e2e\/MyArtifact\/versions\/latest/); + // Make sure we redirected to the artifact page. + await expect(page).toHaveURL(/.+\/explore\/e2e\/MyArtifact/); // Assert the meta-data is as expected await expect(page.getByTestId("artifact-details-name")).toHaveText("No name"); - await expect(page.getByTestId("artifact-details-id")).toHaveText("MyArtifact"); - await expect(page.getByTestId("artifact-details-state")).toHaveText("ENABLED"); + await expect(page.getByTestId("artifact-details-description")).toHaveText("No description"); await expect(page.getByTestId("artifact-details-labels")).toHaveText("No labels"); }); -test("End to End - Edit metadata", async ({ page }) => { +test("End to End - Edit artifact metadata", async ({ page }) => { // Navigate to the artifact details page - await page.goto(`${REGISTRY_UI_URL}/artifacts/e2e/MyArtifact/versions/latest`); + await page.goto(`${REGISTRY_UI_URL}/explore/e2e/MyArtifact`); // Click the "Edit" button to show the modal await page.getByTestId("artifact-btn-edit").click(); await expect(page.getByTestId("edit-metadata-modal-name")).toBeEmpty(); // Change/add some values - await page.getByTestId("edit-metadata-modal-name").fill("Empty API Spec UPDATED"); + await page.getByTestId("edit-metadata-modal-name").fill("Empty API Spec"); await page.getByTestId("edit-metadata-modal-description").fill("A simple empty API."); // Add a label @@ -63,10 +62,8 @@ test("End to End - Edit metadata", async ({ page }) => { await page.reload(); // Assert the meta-data is as expected - await expect(page.getByTestId("artifact-details-name")).toHaveText("Empty API Spec UPDATED"); + await expect(page.getByTestId("artifact-details-name")).toHaveText("Empty API Spec"); await expect(page.getByTestId("artifact-details-description")).toHaveText("A simple empty API."); - await expect(page.getByTestId("artifact-details-id")).toHaveText("MyArtifact"); - await expect(page.getByTestId("artifact-details-state")).toHaveText("ENABLED"); expect(page.getByTestId("artifact-details-labels").getByText("some-key")).toBeDefined(); expect(page.getByTestId("artifact-details-labels").getByText("some-value")).toBeDefined(); }); @@ -75,7 +72,7 @@ test("End to End - Edit metadata", async ({ page }) => { test("End to End - Artifact specific rules", async ({ page }) => { // Navigate to the artifact details page - await page.goto(`${REGISTRY_UI_URL}/artifacts/e2e/MyArtifact/versions/latest`); + await page.goto(`${REGISTRY_UI_URL}/explore/e2e/MyArtifact`); await expect(page.locator("div.rule")).toHaveCount(3); await expect(page.locator("#validity-rule-name")).toContainText("Validity rule"); @@ -96,24 +93,27 @@ test("End to End - Artifact specific rules", async ({ page }) => { }); -test("End to End - Upload new version", async ({ page }) => { +test("End to End - Create new version", async ({ page }) => { // Navigate to the artifact details page - await page.goto(`${REGISTRY_UI_URL}/artifacts/e2e/MyArtifact/versions/latest`); + await page.goto(`${REGISTRY_UI_URL}/explore/e2e/MyArtifact`); + + // Click the "versions" tab + await page.getByTestId("versions-tab").click(); - // Upload a new version - await page.getByTestId("header-btn-upload-version").click(); - await page.locator("#artifact-content").fill(OPENAPI_DATA_V2_STR); - await page.getByTestId("modal-btn-upload").click(); + // Create a new version + await page.getByTestId("btn-toolbar-create-version").click(); + await page.locator("#version-content").fill(OPENAPI_DATA_V2_STR); + await page.getByTestId("modal-btn-create").click(); // Make sure we redirected to the artifact detail page. - await expect(page).toHaveURL(/.+\/artifacts\/e2e\/MyArtifact\/versions\/2/); + await expect(page).toHaveURL(/.+\/explore\/e2e\/MyArtifact\/2/); }); test("End to End - Delete artifact", async ({ page }) => { - await page.goto(`${REGISTRY_UI_URL}/artifacts/e2e/MyArtifact/versions/latest`); + await page.goto(`${REGISTRY_UI_URL}/explore/e2e/MyArtifact`); await page.getByTestId("header-btn-delete").click(); await page.getByTestId("modal-btn-delete").click(); - await expect(page).toHaveURL(/.+\/artifacts/); + await expect(page).toHaveURL(/.+\/explore/); }); diff --git a/ui/ui-app/package-lock.json b/ui/ui-app/package-lock.json index 9f792f7f02..1d53967e5b 100644 --- a/ui/ui-app/package-lock.json +++ b/ui/ui-app/package-lock.json @@ -21,6 +21,7 @@ "buffer": "^6.0.3", "luxon": "3.4.4", "oidc-client-ts": "3.0.1", + "pluralize": "8.0.0", "react": "18.3.1", "react-dom": "18.3.1", "react-router-dom": "6.23.1", @@ -31,6 +32,7 @@ "@apicurio/eslint-config": "0.3.0", "@monaco-editor/react": "4.6.0", "@types/luxon": "3.4.2", + "@types/pluralize": "0.0.33", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "7.10.0", @@ -1352,6 +1354,12 @@ "undici-types": "~5.25.1" } }, + "node_modules/@types/pluralize": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.33.tgz", + "integrity": "sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==", + "dev": true + }, "node_modules/@types/prop-types": { "version": "15.7.8", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.8.tgz", @@ -3131,6 +3139,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "engines": { + "node": ">=4" + } + }, "node_modules/postcss": { "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", diff --git a/ui/ui-app/package.json b/ui/ui-app/package.json index 9c628db281..4740488834 100644 --- a/ui/ui-app/package.json +++ b/ui/ui-app/package.json @@ -16,6 +16,7 @@ "@apicurio/eslint-config": "0.3.0", "@monaco-editor/react": "4.6.0", "@types/luxon": "3.4.2", + "@types/pluralize": "0.0.33", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "7.10.0", @@ -41,6 +42,7 @@ "buffer": "^6.0.3", "luxon": "3.4.4", "oidc-client-ts": "3.0.1", + "pluralize": "8.0.0", "react": "18.3.1", "react-dom": "18.3.1", "react-router-dom": "6.23.1", diff --git a/ui/ui-app/src/app/App.tsx b/ui/ui-app/src/app/App.tsx index 6496c22330..9840f65583 100644 --- a/ui/ui-app/src/app/App.tsx +++ b/ui/ui-app/src/app/App.tsx @@ -6,18 +6,12 @@ import { FunctionComponent } from "react"; import { Page } from "@patternfly/react-core"; import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import { AppHeader } from "@app/components"; -import { - ArtifactRedirectPage, - ArtifactsPage, - ArtifactVersionPage, - NotFoundPage, - RootRedirectPage, - RulesPage -} from "@app/pages"; +import { ExplorePage, GroupPage, NotFoundPage, RootRedirectPage, RulesPage, VersionPage } from "@app/pages"; import { RolesPage, SettingsPage } from "./pages"; import { ConfigService, useConfigService } from "@services/useConfigService.ts"; import { LoggerService, useLoggerService } from "@services/useLoggerService.ts"; import { ApplicationAuth, AuthConfig, AuthConfigContext } from "@apicurio/common-ui-components"; +import { ArtifactPage } from "@app/pages/artifact"; export type AppProps = { // No props @@ -55,14 +49,18 @@ export const App: FunctionComponent = () => { } /> } /> } /> - } /> + } /> } + path="/explore/:groupId" + element={ } /> } + path="/explore/:groupId/:artifactId" + element={ } + /> + } /> } /> diff --git a/ui/ui-app/src/app/components/common/ArtifactDescription.css b/ui/ui-app/src/app/components/common/ArtifactDescription.css new file mode 100644 index 0000000000..ec805a9505 --- /dev/null +++ b/ui/ui-app/src/app/components/common/ArtifactDescription.css @@ -0,0 +1,3 @@ +.no-description { + color: var(--pf-global--Color--200); +} diff --git a/ui/ui-app/src/app/components/common/ArtifactDescription.tsx b/ui/ui-app/src/app/components/common/ArtifactDescription.tsx new file mode 100644 index 0000000000..265e511216 --- /dev/null +++ b/ui/ui-app/src/app/components/common/ArtifactDescription.tsx @@ -0,0 +1,30 @@ +import { CSSProperties, FunctionComponent } from "react"; +import { Truncate } from "@patternfly/react-core"; +import "./ArtifactDescription.css"; + +/** + * Properties + */ +export type ArtifactDescriptionProps = { + description: string | undefined; + truncate?: boolean; + className?: string; + style?: CSSProperties | undefined; +} + +export const ArtifactDescription: FunctionComponent = ({ description, truncate, className, style }: ArtifactDescriptionProps) => { + let classes: string = ""; + if (className) { + classes = className; + } + if (!description) { + classes = classes + " no-description"; + } + return truncate ? ( +
+ +
+ ) : ( +
{description || "No description."}
+ ); +}; diff --git a/ui/ui-app/src/app/components/common/index.ts b/ui/ui-app/src/app/components/common/index.ts index b4ab57b15c..f3ab887bb7 100644 --- a/ui/ui-app/src/app/components/common/index.ts +++ b/ui/ui-app/src/app/components/common/index.ts @@ -1,2 +1,3 @@ -export * from "./ArtifactTypeIcon.tsx"; -export * from "./IfFeature.tsx"; +export * from "./ArtifactTypeIcon"; +export * from "./ArtifactDescription"; +export * from "./IfFeature"; diff --git a/ui/ui-app/src/app/components/header/AppHeader.tsx b/ui/ui-app/src/app/components/header/AppHeader.tsx index 2c6723b4dd..a36a16c4cb 100644 --- a/ui/ui-app/src/app/components/header/AppHeader.tsx +++ b/ui/ui-app/src/app/components/header/AppHeader.tsx @@ -22,7 +22,7 @@ export const AppHeader: FunctionComponent = () => { return ( - }> + }> diff --git a/ui/ui-app/src/app/components/header/RootPageHeader.tsx b/ui/ui-app/src/app/components/header/RootPageHeader.tsx index 20cee1247b..e0cf7b05e6 100644 --- a/ui/ui-app/src/app/components/header/RootPageHeader.tsx +++ b/ui/ui-app/src/app/components/header/RootPageHeader.tsx @@ -22,7 +22,7 @@ export const RootPageHeader: FunctionComponent = (props: Ro if (eventKey !== props.tabKey) { if (eventKey === 0) { // navigate to artifacts - appNavigation.navigateTo("/artifacts"); + appNavigation.navigateTo("/explore"); } if (eventKey === 1) { // navigate to global rules @@ -40,7 +40,7 @@ export const RootPageHeader: FunctionComponent = (props: Ro }; const tabs: any[] = [ - Artifacts} />, + Explore} />, Global rules} /> ]; if (config.featureRoleManagement()) { diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/modals/ChangeOwnerModal.tsx b/ui/ui-app/src/app/components/modals/ChangeOwnerModal.tsx similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/modals/ChangeOwnerModal.tsx rename to ui/ui-app/src/app/components/modals/ChangeOwnerModal.tsx diff --git a/ui/ui-app/src/app/components/modals/ConfirmDeleteModal.tsx b/ui/ui-app/src/app/components/modals/ConfirmDeleteModal.tsx new file mode 100644 index 0000000000..5c9f3aa77b --- /dev/null +++ b/ui/ui-app/src/app/components/modals/ConfirmDeleteModal.tsx @@ -0,0 +1,37 @@ +import { FunctionComponent } from "react"; +import { Button, Modal } from "@patternfly/react-core"; + + +/** + * Properties + */ +export type ConfirmDeleteModalProps = { + title: string; + message: string; + isOpen: boolean; + onDelete: () => void; + onClose: () => void; +}; + +/** + * A modal to prompt the user to delete something. + */ +export const ConfirmDeleteModal: FunctionComponent = (props: ConfirmDeleteModalProps) => { + + return ( + Delete, + + ]} + > +

{ props.message }

+
+ ); + +}; diff --git a/ui/ui-app/src/app/pages/artifacts/components/uploadForm/UploadArtifactForm.css b/ui/ui-app/src/app/components/modals/CreateArtifactForm.css similarity index 100% rename from ui/ui-app/src/app/pages/artifacts/components/uploadForm/UploadArtifactForm.css rename to ui/ui-app/src/app/components/modals/CreateArtifactForm.css diff --git a/ui/ui-app/src/app/pages/artifacts/components/uploadForm/UploadArtifactForm.tsx b/ui/ui-app/src/app/components/modals/CreateArtifactForm.tsx similarity index 64% rename from ui/ui-app/src/app/pages/artifacts/components/uploadForm/UploadArtifactForm.tsx rename to ui/ui-app/src/app/components/modals/CreateArtifactForm.tsx index af9c52c44f..6e04823f77 100644 --- a/ui/ui-app/src/app/pages/artifacts/components/uploadForm/UploadArtifactForm.tsx +++ b/ui/ui-app/src/app/components/modals/CreateArtifactForm.tsx @@ -1,5 +1,5 @@ import { FunctionComponent, useEffect, useState } from "react"; -import "./UploadArtifactForm.css"; +import "./CreateArtifactForm.css"; import { FileUpload, Form, @@ -14,16 +14,20 @@ import { } from "@patternfly/react-core"; import { ExclamationCircleIcon } from "@patternfly/react-icons"; import { If, ObjectSelect, UrlUpload } from "@apicurio/common-ui-components"; -import { CreateArtifactData } from "@services/useGroupsService.ts"; import { UrlService, useUrlService } from "@services/useUrlService.ts"; import { ArtifactTypesService, useArtifactTypesService } from "@services/useArtifactTypesService.ts"; +import { CreateArtifact } from "@models/createArtifact.model.ts"; +import { detectContentType } from "@utils/content.utils.ts"; +import { ContentTypes } from "@models/contentTypes.model.ts"; +import { isStringEmptyOrUndefined } from "@utils/string.utils.ts"; /** * Properties */ -export type UploadArtifactFormProps = { +export type CreateArtifactFormProps = { + groupId?: string; onValid: (valid: boolean) => void; - onChange: (data: CreateArtifactData) => void; + onChange: (groupId: string|null, data: CreateArtifact) => void; }; type ArtifactTypeItem = { @@ -41,18 +45,19 @@ const DIVIDER: ArtifactTypeItem = { }; /** - * Models the toolbar for the Artifacts page. + * Models the Create Artifact modal dialog. */ -export const UploadArtifactForm: FunctionComponent = (props: UploadArtifactFormProps) => { +export const CreateArtifactForm: FunctionComponent = (props: CreateArtifactFormProps) => { const [content, setContent] = useState(); + const [contentType, setContentType] = useState(ContentTypes.APPLICATION_JSON); const [contentIsLoading, setContentIsLoading] = useState(false); - const [id, setId] = useState(""); - const [group, setGroup] = useState(""); - const [type, setType] = useState(""); + const [artifactId, setArtifactId] = useState(""); + const [groupId, setGroupId] = useState(""); + const [artifactType, setArtifactType] = useState(""); const [tabKey, setTabKey] = useState(0); - const [formValid, setFormValid] = useState(false); - const [idValid, setIdValid] = useState(true); - const [groupValid, setGroupValid] = useState(true); + const [isFormValid, setFormValid] = useState(false); + const [isArtifactIdValid, setArtifactIdValid] = useState(true); + const [isGroupIdValid, setGroupIdValid] = useState(true); const [artifactTypes, setArtifactTypes] = useState([]); const [artifactTypeOptions, setArtifactTypeOptions] = useState([]); const [selectedType, setSelectedType] = useState(DEFAULT_ARTIFACT_TYPE); @@ -62,6 +67,7 @@ export const UploadArtifactForm: FunctionComponent = (p const onFileTextChange = (_event: any, value: string | undefined): void => { setContent(value); + setContentType(detectContentType(artifactType, value as string)); }; const onFileClear = (): void => { @@ -76,11 +82,7 @@ export const UploadArtifactForm: FunctionComponent = (p setContentIsLoading(false); }; - const isFormValid = (data: CreateArtifactData): boolean => { - return !!data.content && isIdValid(data.id) && isIdValid(data.groupId); - }; - - const isIdValid = (id: string|null): boolean => { + const checkIdValid = (id: string|null): boolean => { if (!id) { //id is optional, server can generate it return true; @@ -98,30 +100,9 @@ export const UploadArtifactForm: FunctionComponent = (p } }; - const currentData = (): CreateArtifactData => { - return { - content: content, - groupId: group, - id: id, - type: type - }; - }; - - const fireOnChange = (data: CreateArtifactData): void => { - if (props.onChange) { - props.onChange(data); - } - }; - - const fireOnFormValid = (): void => { - if (props.onValid) { - props.onValid(formValid); - } - }; - - const idValidated = (): any => { - if (idValid) { - if (!id) { + const artifactIdValidated = (): any => { + if (isArtifactIdValid) { + if (!artifactId) { return "default"; } return "success"; @@ -131,8 +112,8 @@ export const UploadArtifactForm: FunctionComponent = (p }; const groupValidated = (): any => { - if (groupValid) { - if (!group) { + if (isGroupIdValid) { + if (!groupId) { return "default"; } return "success"; @@ -141,10 +122,31 @@ export const UploadArtifactForm: FunctionComponent = (p } }; + const fireOnChange = (): void => { + const data: CreateArtifact = { + artifactId, + type: artifactType + }; + if (!isStringEmptyOrUndefined(content)) { + data.firstVersion = { + content: { + contentType: contentType, + content: content as string + } + }; + } + const gid: string|null = groupId === "" ? null : groupId; + props.onChange(gid, data); + }; + useEffect(() => { atService.allTypesWithLabels().then(setArtifactTypes); }, []); + useEffect(() => { + setContentType(detectContentType(artifactType, content as string)); + }, [content]); + useEffect(() => { const items: ArtifactTypeItem[] = artifactTypes.map(item => ({ value: item.id, @@ -158,26 +160,36 @@ export const UploadArtifactForm: FunctionComponent = (p }, [artifactTypes]); useEffect(() => { - setType(selectedType.value as string); + setArtifactType(selectedType.value as string); }, [selectedType]); useEffect(() => { - const data: CreateArtifactData = currentData(); + const artifactIdValid: boolean = checkIdValid(artifactId); + const groupIdValid: boolean = checkIdValid(groupId); - setIdValid(isIdValid(id)); - setGroupValid(isIdValid(group)); - setFormValid(isFormValid(data)); - fireOnChange(data); - }, [type, content, id, group]); + setArtifactIdValid(artifactIdValid); + setGroupIdValid(groupIdValid); + let valid: boolean = artifactIdValid && groupIdValid; + + // Note: content can be empty, but if it is then an artifact type MUST be provided (since we cannot detect it from the content). + if (isStringEmptyOrUndefined(content) && isStringEmptyOrUndefined(artifactType)) { + valid = false; + } + + setFormValid(valid); + fireOnChange(); + }, [artifactType, artifactId, groupId, content]); useEffect(() => { - fireOnFormValid(); - }, [formValid]); + if (props.onValid) { + props.onValid(isFormValid); + } + }, [isFormValid]); return (
@@ -186,12 +198,13 @@ export const UploadArtifactForm: FunctionComponent = (p isRequired={false} type="text" id="form-group" - data-testid="upload-artifact-form-group" + data-testid="create-artifact-form-group" name="form-group" aria-describedby="form-group-helper" - value={group} - placeholder="Group" - onChange={(_evt, value) => setGroup(value)} + value={props.groupId || groupId} + placeholder="Group Id" + isDisabled={props.groupId !== undefined} + onChange={(_evt, value) => setGroupId(value)} validated={groupValidated()} /> / @@ -200,16 +213,16 @@ export const UploadArtifactForm: FunctionComponent = (p isRequired={false} type="text" id="form-id" - data-testid="upload-artifact-form-id" + data-testid="create-artifact-form-id" name="form-id" aria-describedby="form-id-helper" - value={id} - placeholder="ID of the artifact" - onChange={(_evt, value) => setId(value)} - validated={idValidated()} + value={artifactId} + placeholder="Artifact Id" + onChange={(_evt, value) => setArtifactId(value)} + validated={artifactIdValidated()} />
- + }>Character % and non ASCII characters are not allowed @@ -218,30 +231,30 @@ export const UploadArtifactForm: FunctionComponent = (p - (Optional) Group and Artifact ID are optional. If Artifact ID is left blank, the server will generate one for you. + (Optional) Group Id and Artifact Id are optional. If Artifact Id is left blank, the server will generate one for you.
item.isDivider} - itemToTestId={(item) => `upload-artifact-form-${item.value}`} + itemToTestId={(item) => `create-artifact-form-${item.value}`} itemToString={(item) => item.label} />
= (p > From file} aria-label="Upload from file" > = (p From URL} aria-label="Upload from URL" > { onFileTextChange(null, value); }} diff --git a/ui/ui-app/src/app/components/modals/CreateArtifactModal.tsx b/ui/ui-app/src/app/components/modals/CreateArtifactModal.tsx new file mode 100644 index 0000000000..533e28cf09 --- /dev/null +++ b/ui/ui-app/src/app/components/modals/CreateArtifactModal.tsx @@ -0,0 +1,67 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import { Button, Modal } from "@patternfly/react-core"; +import { CreateArtifact } from "@models/createArtifact.model.ts"; +import { CreateArtifactForm } from "@app/components"; + +const EMPTY_FORM_DATA: CreateArtifact = { +}; + +/** + * Properties + */ +export type CreateArtifactModalProps = { + groupId?: string; + isOpen: boolean; + onClose: () => void; + onCreate: (groupId: string|null, data: CreateArtifact) => void; +}; + +/** + * Models the Create Artifact modal dialog. + */ +export const CreateArtifactModal: FunctionComponent = (props: CreateArtifactModalProps) => { + const [isFormValid, setFormValid] = useState(false); + const [groupId, setGroupId] = useState(null); + const [formData, setFormData] = useState(EMPTY_FORM_DATA); + + const onCreateArtifactFormValid = (isValid: boolean): void => { + setFormValid(isValid); + }; + + const onCreateArtifactFormChange = (groupId: string|null, data: CreateArtifact): void => { + setGroupId(groupId); + setFormData(data); + }; + + const fireCloseEvent = (): void => { + props.onClose(); + }; + + const fireCreateEvent = (): void => { + props.onCreate(groupId, formData); + }; + + useEffect(() => { + if (props.isOpen) { + setFormValid(false); + setFormData(EMPTY_FORM_DATA); + } + }, [props.isOpen]); + + return ( + Create, + + ]} + > + + + ); + +}; diff --git a/ui/ui-app/src/app/components/modals/CreateGroupModal.tsx b/ui/ui-app/src/app/components/modals/CreateGroupModal.tsx new file mode 100644 index 0000000000..f2aa988c4b --- /dev/null +++ b/ui/ui-app/src/app/components/modals/CreateGroupModal.tsx @@ -0,0 +1,82 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import { Button, Form, FormGroup, Grid, GridItem, Modal, TextInput } from "@patternfly/react-core"; +import { CreateGroup } from "@models/createGroup.model.ts"; + + +/** + * Properties + */ +export type CreateGroupModalProps = { + isOpen: boolean; + onClose: () => void; + onCreate: (data: CreateGroup) => void; +}; + +/** + * Models the Create Group modal dialog. + */ +export const CreateGroupModal: FunctionComponent = (props: CreateGroupModalProps) => { + const [isFormValid, setFormValid] = useState(false); + const [groupId, setGroupId] = useState(""); + + useEffect(() => { + setFormValid(groupId !== null && groupId.trim().length > 0); + }, [groupId]); + + const reset = (): void => { + setFormValid(false); + setGroupId(""); + }; + + const fireCloseEvent = (): void => { + props.onClose(); + reset(); + }; + + const fireCreateEvent = (): void => { + const data: CreateGroup = { + groupId + }; + props.onCreate(data); + reset(); + }; + + return ( + Create, + + ]} + > + + + + + setGroupId(value)} + /> + + + + + + ); + +}; diff --git a/ui/ui-app/src/app/components/modals/CreateVersionModal.tsx b/ui/ui-app/src/app/components/modals/CreateVersionModal.tsx new file mode 100644 index 0000000000..6a2343ecb5 --- /dev/null +++ b/ui/ui-app/src/app/components/modals/CreateVersionModal.tsx @@ -0,0 +1,129 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import { Button, FileUpload, Form, FormGroup, Modal, TextInput } from "@patternfly/react-core"; +import { CreateVersion } from "@models/createVersion.model.ts"; +import { isStringEmptyOrUndefined } from "@utils/string.utils.ts"; +import { detectContentType } from "@utils/content.utils.ts"; + + +/** + * Labels + */ +export type CreateVersionModalProps = { + artifactType: string; + isOpen: boolean; + onClose: () => void; + onCreate: (data: CreateVersion) => void; +}; + +/** + * Models the create version dialog. + */ +export const CreateVersionModal: FunctionComponent = (props: CreateVersionModalProps) => { + const [version, setVersion] = useState(""); + const [content, setContent] = useState(""); + const [contentFilename] = useState(""); + const [contentIsLoading, setContentIsLoading] = useState(false); + const [isFormValid, setFormValid] = useState(false); + + const onContentChange = (_event: any, value: any): void => { + setContent(value); + }; + + const onFileReadStarted = (): void => { + setContentIsLoading(true); + }; + + const onFileReadFinished = (): void => { + setContentIsLoading(false); + }; + + const checkValid = (): void => { + const newValid: boolean = isValid(content); + setFormValid(newValid); + }; + + const isValid = (data: string): boolean => { + return !!data; + }; + + const onCreate = (): void => { + const data: CreateVersion = { + version: isStringEmptyOrUndefined(version) ? undefined : version, + content: { + contentType: detectContentType(props.artifactType, content), + content: content + } + }; + props.onCreate(data); + }; + + useEffect(() => { + checkValid(); + }, [content]); + + return ( + Create, + + ]} + > +
+ + { + setVersion(value); + }} + /> + + + onContentChange({}, "")} + isLoading={contentIsLoading} + /> + +
+
+ ); +}; diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/modals/EditMetaDataModal.css b/ui/ui-app/src/app/components/modals/EditMetaDataModal.css similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/modals/EditMetaDataModal.css rename to ui/ui-app/src/app/components/modals/EditMetaDataModal.css diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/modals/EditMetaDataModal.tsx b/ui/ui-app/src/app/components/modals/EditMetaDataModal.tsx similarity index 57% rename from ui/ui-app/src/app/pages/artifactVersion/components/modals/EditMetaDataModal.tsx rename to ui/ui-app/src/app/components/modals/EditMetaDataModal.tsx index e485ddb487..747f36e419 100644 --- a/ui/ui-app/src/app/pages/artifactVersion/components/modals/EditMetaDataModal.tsx +++ b/ui/ui-app/src/app/components/modals/EditMetaDataModal.tsx @@ -10,55 +10,76 @@ import { TextArea, TextInput } from "@patternfly/react-core"; -import { ArtifactLabel, listToLabels, LabelsFormGroup, labelsToList } from "@app/pages"; -import { EditableMetaData } from "@services/useGroupsService.ts"; +import { If } from "@apicurio/common-ui-components"; +import { ArtifactLabel, LabelsFormGroup } from "@app/components"; + + +export type MetaData = { + name?: string; + description: string; + labels: { [key: string]: string|undefined }; +} + + +function labelsToList(labels: { [key: string]: string|undefined }): ArtifactLabel[] { + return Object.keys(labels).filter((key) => key !== undefined).map(key => { + return { + name: key, + value: labels[key], + nameValidated: "default", + valueValidated: "default" + }; + }); +} + +function listToLabels(labels: ArtifactLabel[]): { [key: string]: string|undefined } { + const rval: { [key: string]: string|undefined } = {}; + labels.forEach(label => { + if (label.name) { + rval[label.name] = label.value; + } + }); + return rval; +} /** * Labels */ export type EditMetaDataModalProps = { - name: string; + entityType: string; + name?: string; description: string; labels: { [key: string]: string|undefined }; isOpen: boolean; onClose: () => void; - onEditMetaData: (metaData: EditableMetaData) => void; + onEditMetaData: (metaData: MetaData) => void; }; /** * Models the edit meta data dialog. */ export const EditMetaDataModal: FunctionComponent = (props: EditMetaDataModalProps) => { + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); const [labels, setLabels] = useState([]); const [isValid, setIsValid] = useState(true); - const [metaData, setMetaData] = useState({ - description: "", - labels: {}, - name: "" - }); - const doEdit = (): void => { - const newMetaData: EditableMetaData = { - ...metaData, + const data: MetaData = { + name, + description, labels: listToLabels(labels) }; - props.onEditMetaData(newMetaData); + props.onEditMetaData(data); }; const onNameChange = (_event: any, value: string): void => { - setMetaData({ - ...metaData, - name: value - }); + setName(value); }; const onDescriptionChange = (_event: any, value: string): void => { - setMetaData({ - ...metaData, - description: value - }); + setDescription(value); }; const onLabelsChange = (labels: ArtifactLabel[]): void => { @@ -90,27 +111,24 @@ export const EditMetaDataModal: FunctionComponent = (pro useEffect(() => { validate(); - }, [labels, metaData]); + }, [name, description, labels]); useEffect(() => { if (props.isOpen) { setLabels(labelsToList(props.labels)); - setMetaData({ - description: props.description, - labels: props.labels, - name: props.name - }); + setName(props.name); + setDescription(props.description); setIsValid(true); } }, [props.isOpen]); return ( Save, @@ -118,24 +136,26 @@ export const EditMetaDataModal: FunctionComponent = (pro >
- - - - - + + + + + + + = (pro data-testid="edit-metadata-modal-description" name="form-description" aria-describedby="form-description-helper" - value={metaData.description} - placeholder="Description of the artifact" + value={description} + placeholder={`Description of the ${props.entityType}`} onChange={onDescriptionChange} /> diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/modals/LabelsFormGroup.tsx b/ui/ui-app/src/app/components/modals/LabelsFormGroup.tsx similarity index 97% rename from ui/ui-app/src/app/pages/artifactVersion/components/modals/LabelsFormGroup.tsx rename to ui/ui-app/src/app/components/modals/LabelsFormGroup.tsx index 63ad106813..58e025a7b8 100644 --- a/ui/ui-app/src/app/pages/artifactVersion/components/modals/LabelsFormGroup.tsx +++ b/ui/ui-app/src/app/components/modals/LabelsFormGroup.tsx @@ -9,6 +9,7 @@ export type ArtifactLabel = { valueValidated: "success" | "warning" | "error" | "default"; } + /** * Labels */ @@ -37,7 +38,7 @@ export const LabelsFormGroup: FunctionComponent = ({ label return ( - + { labels.map((label, idx) => ( diff --git a/ui/ui-app/src/app/components/modals/index.ts b/ui/ui-app/src/app/components/modals/index.ts index 8d4a31c054..c68e4fbc91 100644 --- a/ui/ui-app/src/app/components/modals/index.ts +++ b/ui/ui-app/src/app/components/modals/index.ts @@ -1 +1,9 @@ -export * from "./InvalidContentModal.tsx"; +export * from "./ChangeOwnerModal"; +export * from "./ConfirmDeleteModal"; +export * from "./CreateArtifactForm"; +export * from "./CreateArtifactModal"; +export * from "./CreateGroupModal"; +export * from "./CreateVersionModal"; +export * from "./EditMetaDataModal"; +export * from "./InvalidContentModal"; +export * from "./LabelsFormGroup"; diff --git a/ui/ui-app/src/app/pages/404/404.tsx b/ui/ui-app/src/app/pages/404/404.tsx index 19d704ea40..597a29bdd2 100644 --- a/ui/ui-app/src/app/pages/404/404.tsx +++ b/ui/ui-app/src/app/pages/404/404.tsx @@ -39,7 +39,7 @@ export const NotFoundPage: FunctionComponent = () => { + onClick={() => appNavigation.navigateTo("/explore")}>Show all artifacts diff --git a/ui/ui-app/src/app/pages/artifact/ArtifactPage.css b/ui/ui-app/src/app/pages/artifact/ArtifactPage.css new file mode 100644 index 0000000000..fe724caf38 --- /dev/null +++ b/ui/ui-app/src/app/pages/artifact/ArtifactPage.css @@ -0,0 +1,32 @@ +div.artifact-page-tabs > ul > li.pf-c-tabs__item.pf-m-current > button.pf-c-tabs__button::before { + border-bottom-color: transparent; +} + +.artifact-details-main { + display: flex; + flex-direction: column; +} + +.artifact-details-main .pf-c-tab-content { + flex-grow: 1; + display: flex; + flex-direction: column; +} + +#artifact-page-tabs > ul > li:first-child { + margin-left: 20px; +} + +.ps_header-breadcrumbs { + padding: 12px 24px 0; +} + +.pf-v5-c-breadcrumb__item-divider { + color: #666; +} + +#pf-tab-section-content-artifact-page-tabs { + display: flex; + flex-grow: 1; + position: relative; +} diff --git a/ui/ui-app/src/app/pages/artifact/ArtifactPage.tsx b/ui/ui-app/src/app/pages/artifact/ArtifactPage.tsx new file mode 100644 index 0000000000..688b63c44c --- /dev/null +++ b/ui/ui-app/src/app/pages/artifact/ArtifactPage.tsx @@ -0,0 +1,349 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import "./ArtifactPage.css"; +import { Breadcrumb, BreadcrumbItem, PageSection, PageSectionVariants, Tab, Tabs } from "@patternfly/react-core"; +import { Link, useParams } from "react-router-dom"; +import { ArtifactMetaData } from "@models/artifactMetaData.model.ts"; +import { Rule } from "@models/rule.model.ts"; +import { PageDataLoader, PageError, PageErrorHandler, toPageError } from "@app/pages"; +import { + ChangeOwnerModal, + ConfirmDeleteModal, + CreateVersionModal, + EditMetaDataModal, + IfFeature, + InvalidContentModal, MetaData +} from "@app/components"; +import { PleaseWaitModal } from "@apicurio/common-ui-components"; +import { AppNavigation, useAppNavigation } from "@services/useAppNavigation.ts"; +import { LoggerService, useLoggerService } from "@services/useLoggerService.ts"; +import { GroupsService, useGroupsService } from "@services/useGroupsService.ts"; +import { + ArtifactInfoTabContent, + ArtifactPageHeader, + VersionsTabContent +} from "@app/pages/artifact/components"; +import { SearchedVersion } from "@models/searchedVersion.model.ts"; +import { CreateVersion } from "@models/createVersion.model.ts"; +import { ApiError } from "@models/apiError.model.ts"; + + +export type ArtifactPageProps = { + // No properties +} + +/** + * The artifact version page. + */ +export const ArtifactPage: FunctionComponent = () => { + const [pageError, setPageError] = useState(); + const [loaders, setLoaders] = useState | Promise[] | undefined>(); + const [activeTabKey, setActiveTabKey] = useState("overview"); + const [artifact, setArtifact] = useState(); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isDeleteVersionModalOpen, setIsDeleteVersionModalOpen] = useState(false); + const [isCreateVersionModalOpen, setIsCreateVersionModalOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isChangeOwnerModalOpen, setIsChangeOwnerModalOpen] = useState(false); + const [isPleaseWaitModalOpen, setIsPleaseWaitModalOpen] = useState(false); + const [pleaseWaitMessage, setPleaseWaitMessage] = useState(""); + const [rules, setRules] = useState([]); + const [invalidContentError, setInvalidContentError] = useState(); + const [isInvalidContentModalOpen, setIsInvalidContentModalOpen] = useState(false); + const [versionToDelete, setVersionToDelete] = useState(); + const [versionDeleteSuccessCallback, setVersionDeleteSuccessCallback] = useState<() => void>(); + + const appNavigation: AppNavigation = useAppNavigation(); + const logger: LoggerService = useLoggerService(); + const groups: GroupsService = useGroupsService(); + const { groupId, artifactId }= useParams(); + + const createLoaders = (): Promise[] => { + let gid: string|null = groupId as string; + if (gid == "default") { + gid = null; + } + logger.info("Loading data for artifact: ", artifactId); + return [ + groups.getArtifactMetaData(gid, artifactId as string) + .then(setArtifact) + .catch(error => { + setPageError(toPageError(error, "Error loading page data.")); + }), + groups.getArtifactRules(gid, artifactId as string) + .then(setRules) + .catch(error => { + setPageError(toPageError(error, "Error loading page data.")); + }), + ]; + }; + + const handleTabClick = (_event: any, tabIndex: any): void => { + setActiveTabKey(tabIndex); + }; + + const onDeleteArtifact = (): void => { + setIsDeleteModalOpen(true); + }; + + const doEnableRule = (ruleType: string): void => { + logger.debug("[ArtifactPage] Enabling rule:", ruleType); + let config: string = "FULL"; + if (ruleType === "COMPATIBILITY") { + config = "BACKWARD"; + } + groups.createArtifactRule(groupId as string, artifactId as string, ruleType, config).catch(error => { + setPageError(toPageError(error, `Error enabling "${ ruleType }" artifact rule.`)); + }); + setRules([...rules, { config, type: ruleType }]); + }; + + const doDisableRule = (ruleType: string): void => { + logger.debug("[ArtifactPage] Disabling rule:", ruleType); + groups.deleteArtifactRule(groupId as string, artifactId as string, ruleType).catch(error => { + setPageError(toPageError(error, `Error disabling "${ ruleType }" artifact rule.`)); + }); + setRules(rules.filter(r => r.type !== ruleType)); + }; + + const doConfigureRule = (ruleType: string, config: string): void => { + logger.debug("[ArtifactPage] Configuring rule:", ruleType, config); + groups.updateArtifactRule(groupId as string, artifactId as string, ruleType, config).catch(error => { + setPageError(toPageError(error, `Error configuring "${ ruleType }" artifact rule.`)); + }); + setRules(rules.map(r => { + if (r.type === ruleType) { + return { config, type: r.type }; + } else { + return r; + } + })); + }; + + const onDeleteModalClose = (): void => { + setIsDeleteModalOpen(false); + }; + + const onDeleteVersionModalClose = (): void => { + setIsDeleteVersionModalOpen(false); + }; + + const doDeleteArtifact = (): void => { + onDeleteModalClose(); + pleaseWait(true, "Deleting artifact, please wait..."); + groups.deleteArtifact(groupId as string, artifactId as string).then( () => { + pleaseWait(false, ""); + appNavigation.navigateTo("/explore"); + }); + }; + + const openEditMetaDataModal = (): void => { + setIsEditModalOpen(true); + }; + + const openChangeOwnerModal = (): void => { + setIsChangeOwnerModalOpen(true); + }; + + const onEditModalClose = (): void => { + setIsEditModalOpen(false); + }; + + const onChangeOwnerModalClose = (): void => { + setIsChangeOwnerModalOpen(false); + }; + + const doEditMetaData = (metaData: MetaData): void => { + groups.updateArtifactMetaData(groupId as string, artifactId as string, metaData).then( () => { + if (artifact) { + setArtifact({ + ...artifact, + ...metaData + }); + } + }).catch( error => { + setPageError(toPageError(error, "Error editing artifact metadata.")); + }); + onEditModalClose(); + }; + + const doChangeOwner = (newOwner: string): void => { + groups.updateArtifactOwner(groupId as string, artifactId as string, newOwner).then( () => { + if (artifact) { + setArtifact({ + ...artifact, + owner: newOwner + }); + } + }).catch( error => { + setPageError(toPageError(error, "Error changing artifact ownership.")); + }); + onChangeOwnerModalClose(); + }; + + const onViewVersion = (version: SearchedVersion): void => { + const groupId: string = encodeURIComponent(artifact?.groupId || "default"); + const artifactId: string = encodeURIComponent(artifact?.artifactId || ""); + const ver: string = encodeURIComponent(version.version); + appNavigation.navigateTo(`/explore/${groupId}/${artifactId}/${ver}`); + }; + + const onDeleteVersion = (version: SearchedVersion, successCallback?: () => void): void => { + setVersionToDelete(version); + setIsDeleteVersionModalOpen(true); + setVersionDeleteSuccessCallback(() => successCallback); + }; + + const doDeleteVersion = (): void => { + setIsDeleteVersionModalOpen(false); + pleaseWait(true, "Deleting version, please wait..."); + groups.deleteArtifactVersion(groupId as string, artifactId as string, versionToDelete?.version as string).then( () => { + pleaseWait(false); + if (versionDeleteSuccessCallback) { + versionDeleteSuccessCallback(); + } + }).catch(error => { + setPageError(toPageError(error, "Error deleting a version.")); + }); + }; + + const handleInvalidContentError = (error: any): void => { + logger.info("INVALID CONTENT ERROR", error); + setInvalidContentError(error); + setIsInvalidContentModalOpen(true); + }; + + const doCreateArtifactVersion = (data: CreateVersion): void => { + setIsCreateVersionModalOpen(false); + pleaseWait(true, "Creating a new version, please wait..."); + + groups.createArtifactVersion(groupId as string, artifactId as string, data).then(versionMetaData => { + const groupId: string = encodeURIComponent(versionMetaData.groupId ? versionMetaData.groupId : "default"); + const artifactId: string = encodeURIComponent(versionMetaData.artifactId); + const version: string = encodeURIComponent(versionMetaData.version); + const artifactVersionLocation: string = `/explore/${groupId}/${artifactId}/${version}`; + logger.info("[ArtifactPage] Artifact version successfully created. Redirecting to details: ", artifactVersionLocation); + pleaseWait(false); + appNavigation.navigateTo(artifactVersionLocation); + }).catch( error => { + pleaseWait(false); + if (error && (error.error_code === 400 || error.error_code === 409)) { + handleInvalidContentError(error); + } else { + setPageError(toPageError(error, "Error creating artifact version.")); + } + }); + }; + + const pleaseWait = (isOpen: boolean, message: string = ""): void => { + setIsPleaseWaitModalOpen(isOpen); + setPleaseWaitMessage(message); + }; + + useEffect(() => { + setLoaders(createLoaders()); + }, [groupId, artifactId]); + + const tabs: any[] = [ + + + , + + {setIsCreateVersionModalOpen(true);}} + onViewVersion={onViewVersion} + onDeleteVersion={onDeleteVersion} + /> + , + ]; + + const gid: string = groupId || "default"; + const hasGroup: boolean = gid != "default"; + let breadcrumbs = ( + + Explore + { gid } + { artifactId as string } + + ); + if (!hasGroup) { + breadcrumbs = ( + + Explore + { artifactId as string } + + ); + } + + return ( + + + + + + + + + + + + + + + + setIsCreateVersionModalOpen(false)} + onCreate={doCreateArtifactVersion} + /> + + + {setIsInvalidContentModalOpen(false);}} /> + + ); + +}; diff --git a/ui/ui-app/src/app/pages/artifact/ArtifactRedirectPage.tsx b/ui/ui-app/src/app/pages/artifact/ArtifactRedirectPage.tsx deleted file mode 100644 index 3a28ff059a..0000000000 --- a/ui/ui-app/src/app/pages/artifact/ArtifactRedirectPage.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { FunctionComponent } from "react"; -import { Navigate, useParams } from "react-router-dom"; -import { AppNavigation, useAppNavigation } from "@services/useAppNavigation.ts"; -import { useLoggerService } from "@services/useLoggerService.ts"; - - -/** - * Properties - */ -export type ArtifactRedirectPageProps = { - // No properties. -} - -/** - * The artifact redirect page. - */ -//export class ArtifactRedirectPage extends PageComponent { -export const ArtifactRedirectPage: FunctionComponent = () => { - const params = useParams(); - const appNavigation: AppNavigation = useAppNavigation(); - const logger = useLoggerService(); - - const groupId: string = params["groupId"] || ""; - const artifactId: any = params["artifactId"] || ""; - - const redirect: string = appNavigation.createLink(`/artifacts/${ encodeURIComponent(groupId) }/${ encodeURIComponent(artifactId) }/versions/latest`); - logger.info("[ArtifactRedirectPage] Redirecting to: %s", redirect); - return ( - - ); -}; diff --git a/ui/ui-app/src/app/pages/artifact/components/index.ts b/ui/ui-app/src/app/pages/artifact/components/index.ts new file mode 100644 index 0000000000..cc8a7498bd --- /dev/null +++ b/ui/ui-app/src/app/pages/artifact/components/index.ts @@ -0,0 +1,2 @@ +export * from "./pageheader"; +export * from "./tabs"; diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/ArtifactVersionPageHeader.css b/ui/ui-app/src/app/pages/artifact/components/pageheader/ArtifactPageHeader.css similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/pageheader/ArtifactVersionPageHeader.css rename to ui/ui-app/src/app/pages/artifact/components/pageheader/ArtifactPageHeader.css diff --git a/ui/ui-app/src/app/pages/artifact/components/pageheader/ArtifactPageHeader.tsx b/ui/ui-app/src/app/pages/artifact/components/pageheader/ArtifactPageHeader.tsx new file mode 100644 index 0000000000..c7cfcc09da --- /dev/null +++ b/ui/ui-app/src/app/pages/artifact/components/pageheader/ArtifactPageHeader.tsx @@ -0,0 +1,44 @@ +import { FunctionComponent } from "react"; +import "./ArtifactPageHeader.css"; +import { Button, Flex, FlexItem, Text, TextContent, TextVariants } from "@patternfly/react-core"; +import { IfAuth, IfFeature } from "@app/components"; +import { ArtifactMetaData } from "@models/artifactMetaData.model.ts"; +import { If } from "@apicurio/common-ui-components"; + + +/** + * Properties + */ +export type ArtifactPageHeaderProps = { + artifact: ArtifactMetaData; + onDeleteArtifact: () => void; +}; + +/** + * Models the page header for the Artifact page. + */ +export const ArtifactPageHeader: FunctionComponent = (props: ArtifactPageHeaderProps) => { + return ( + + + + + + {props.artifact.groupId} + / + + {props.artifact.artifactId} + + + + + + + + + + + + ); +}; diff --git a/ui/ui-app/src/app/pages/artifact/components/pageheader/index.ts b/ui/ui-app/src/app/pages/artifact/components/pageheader/index.ts new file mode 100644 index 0000000000..b9087bcdb9 --- /dev/null +++ b/ui/ui-app/src/app/pages/artifact/components/pageheader/index.ts @@ -0,0 +1 @@ +export * from "./ArtifactPageHeader.tsx"; diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/InfoTabContent.css b/ui/ui-app/src/app/pages/artifact/components/tabs/ArtifactInfoTabContent.css similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/InfoTabContent.css rename to ui/ui-app/src/app/pages/artifact/components/tabs/ArtifactInfoTabContent.css diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/InfoTabContent.tsx b/ui/ui-app/src/app/pages/artifact/components/tabs/ArtifactInfoTabContent.tsx similarity index 70% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/InfoTabContent.tsx rename to ui/ui-app/src/app/pages/artifact/components/tabs/ArtifactInfoTabContent.tsx index 07bbab61c0..760d17e181 100644 --- a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/InfoTabContent.tsx +++ b/ui/ui-app/src/app/pages/artifact/components/tabs/ArtifactInfoTabContent.tsx @@ -1,5 +1,5 @@ import { FunctionComponent } from "react"; -import "./InfoTabContent.css"; +import "./ArtifactInfoTabContent.css"; import "@app/styles/empty.css"; import { ArtifactTypeIcon, IfAuth, IfFeature, RuleList } from "@app/components"; import { @@ -17,24 +17,21 @@ import { Label, Truncate } from "@patternfly/react-core"; -import { DownloadIcon, PencilAltIcon } from "@patternfly/react-icons"; +import { PencilAltIcon } from "@patternfly/react-icons"; import { Rule } from "@models/rule.model.ts"; import { FromNow, If } from "@apicurio/common-ui-components"; -import { VersionMetaData } from "@models/versionMetaData.model.ts"; import { ArtifactMetaData } from "@models/artifactMetaData.model.ts"; +import { isStringEmptyOrUndefined } from "@utils/string.utils.ts"; /** * Properties */ -export type InfoTabContentProps = { +export type ArtifactInfoTabContentProps = { artifact: ArtifactMetaData; - version: VersionMetaData; - isLatest: boolean; rules: Rule[]; onEnableRule: (ruleType: string) => void; onDisableRule: (ruleType: string) => void; onConfigureRule: (ruleType: string, config: string) => void; - onDownloadArtifact: () => void; onEditMetaData: () => void; onChangeOwner: () => void; }; @@ -42,14 +39,14 @@ export type InfoTabContentProps = { /** * Models the content of the Artifact Info tab. */ -export const InfoTabContent: FunctionComponent = (props: InfoTabContentProps) => { +export const ArtifactInfoTabContent: FunctionComponent = (props: ArtifactInfoTabContentProps) => { const description = (): string => { - return props.version.description || "No description"; + return props.artifact.description || "No description"; }; const artifactName = (): string => { - return props.version.name || "No name"; + return props.artifact.name || "No name"; }; return ( @@ -60,7 +57,7 @@ export const InfoTabContent: FunctionComponent = (props: In
- Version metadata + Artifact metadata @@ -81,41 +78,33 @@ export const InfoTabContent: FunctionComponent = (props: In Name { artifactName() } - - ID - {props.version.artifactId} - Description { description() } - - Status - {props.version.state} - Created - + - + Owner - {props.version.owner} + {props.artifact.owner} - + -
diff --git a/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTabContent.css b/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTabContent.css new file mode 100644 index 0000000000..28c110c7e8 --- /dev/null +++ b/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTabContent.css @@ -0,0 +1,10 @@ +.versions-tab-content { + padding: 22px; + display: flex; + flex-direction: column; + background-color: rgb(240,240,240); +} + +.versions-tab-content > div { + background-color: white; +} diff --git a/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTabContent.tsx b/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTabContent.tsx new file mode 100644 index 0000000000..5050dee7d9 --- /dev/null +++ b/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTabContent.tsx @@ -0,0 +1,134 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import "./VersionsTabContent.css"; +import "@app/styles/empty.css"; +import { ListWithToolbar } from "@apicurio/common-ui-components"; +import { Paging } from "@models/paging.model.ts"; +import { LoggerService, useLoggerService } from "@services/useLoggerService.ts"; +import { + Button, + EmptyState, + EmptyStateActions, + EmptyStateBody, + EmptyStateFooter, + EmptyStateIcon, + EmptyStateVariant, + Title +} from "@patternfly/react-core"; +import { PlusCircleIcon } from "@patternfly/react-icons"; +import { IfAuth, IfFeature } from "@app/components"; +import { GroupsService, useGroupsService } from "@services/useGroupsService.ts"; +import { SortOrder } from "@models/sortOrder.model.ts"; +import { ArtifactMetaData } from "@models/artifactMetaData.model.ts"; +import { VersionSortBy } from "@models/versionSortBy.model.ts"; +import { VersionsTable, VersionsTabToolbar } from "@app/pages/artifact"; +import { VersionSearchResults } from "@models/versionSearchResults.model.ts"; +import { SearchedVersion } from "@models/searchedVersion.model.ts"; + +/** + * Properties + */ +export type VersionsTabContentProps = { + artifact: ArtifactMetaData; + onCreateVersion: () => void; + onViewVersion: (version: SearchedVersion) => void; + onDeleteVersion: (version: SearchedVersion, deleteSuccessCallback: () => void) => void; +}; + +/** + * Models the content of the Version Info tab. + */ +export const VersionsTabContent: FunctionComponent = (props: VersionsTabContentProps) => { + const [isLoading, setLoading] = useState(true); + const [isError, setError] = useState(false); + const [paging, setPaging] = useState({ + page: 1, + pageSize: 20 + }); + const [sortBy, setSortBy] = useState(VersionSortBy.globalId); + const [sortOrder, setSortOrder] = useState(SortOrder.asc); + const [results, setResults] = useState({ + count: 0, + versions: [] + }); + + const groups: GroupsService = useGroupsService(); + const logger: LoggerService = useLoggerService(); + + const refresh = (): void => { + setLoading(true); + + groups.getArtifactVersions(props.artifact.groupId, props.artifact.artifactId, sortBy, sortOrder, paging).then(sr => { + setResults(sr); + setLoading(false); + }).catch(error => { + logger.error(error); + setLoading(false); + setError(true); + }); + }; + + const onSort = (by: VersionSortBy, order: SortOrder): void => { + setSortBy(by); + setSortOrder(order); + }; + + const onDeleteVersion = (version: SearchedVersion): void => { + props.onDeleteVersion(version, () => { + setTimeout(refresh, 100); + }); + }; + + useEffect(() => { + refresh(); + }, [props.artifact, paging, sortBy, sortOrder]); + + const toolbar = ( + + ); + + const emptyState = ( + + + No versions found + + There are currently no versions in this artifact. Create some versions in the artifact to view them here. + + + + + + + + + + + + ); + + return ( +
+
+ + + +
+
+ ); + +}; diff --git a/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTabToolbar.css b/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTabToolbar.css new file mode 100644 index 0000000000..1a9d6ebede --- /dev/null +++ b/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTabToolbar.css @@ -0,0 +1,17 @@ +.versions-toolbar { + padding-left: 8px; + padding-right: 24px; +} + +.versions-toolbar > div { + width: 100%; +} + +.versions-toolbar .filter-types-toggle span.pf-v5-c-menu-toggle__text { + width: 125px; + text-align: left; +} + +#versions-toolbar-1 .tbi-filter-type { + margin-right: 0; +} diff --git a/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTabToolbar.tsx b/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTabToolbar.tsx new file mode 100644 index 0000000000..ed867f3d04 --- /dev/null +++ b/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTabToolbar.tsx @@ -0,0 +1,68 @@ +import { FunctionComponent } from "react"; +import "./VersionsTabToolbar.css"; +import { Button, Pagination, Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core"; +import { Paging } from "@models/paging.model.ts"; +import { IfAuth, IfFeature } from "@app/components"; +import { VersionSearchResults } from "@models/versionSearchResults.model.ts"; + + +/** + * Properties + */ +export type VersionsToolbarProps = { + results: VersionSearchResults; + paging: Paging; + onPageChange: (paging: Paging) => void; + onCreateVersion: () => void; +}; + + +/** + * Models the toolbar for the Versions tab on the Artifact page. + */ +export const VersionsTabToolbar: FunctionComponent = (props: VersionsToolbarProps) => { + + const onSetPage = (_event: any, newPage: number, perPage?: number): void => { + const newPaging: Paging = { + page: newPage, + pageSize: perPage ? perPage : props.paging.pageSize + }; + props.onPageChange(newPaging); + }; + + const onPerPageSelect = (_event: any, newPerPage: number): void => { + const newPaging: Paging = { + page: props.paging.page, + pageSize: newPerPage + }; + props.onPageChange(newPaging); + }; + + return ( + + + + + + + + + + + + + + + ); +}; diff --git a/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTable.tsx b/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTable.tsx new file mode 100644 index 0000000000..b4bb8d1c2e --- /dev/null +++ b/ui/ui-app/src/app/pages/artifact/components/tabs/VersionsTable.tsx @@ -0,0 +1,160 @@ +import React, { FunctionComponent, useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { SortByDirection, ThProps } from "@patternfly/react-table"; +import { FromNow, If, ObjectDropdown, ResponsiveTable } from "@apicurio/common-ui-components"; +import { AppNavigation, useAppNavigation } from "@services/useAppNavigation.ts"; +import { shash } from "@utils/string.utils.ts"; +import { SortOrder } from "@models/sortOrder.model.ts"; +import { SearchedVersion } from "@models/searchedVersion.model.ts"; +import { ArtifactMetaData } from "@models/artifactMetaData.model.ts"; +import { VersionSortBy } from "@models/versionSortBy.model.ts"; +import { ArtifactDescription } from "@app/components"; + +export type VersionsTableProps = { + artifact: ArtifactMetaData; + versions: SearchedVersion[]; + sortBy: VersionSortBy; + sortOrder: SortOrder; + onSort: (by: VersionSortBy, order: SortOrder) => void; + onView: (version: SearchedVersion) => void; + onDelete: (version: SearchedVersion) => void; +} +type VersionAction = { + label: string; + testId: string; + onClick: () => void; +}; + +type VersionActionSeparator = { + isSeparator: true; +}; + +export const VersionsTable: FunctionComponent = (props: VersionsTableProps) => { + const [sortByIndex, setSortByIndex] = useState(); + + const appNavigation: AppNavigation = useAppNavigation(); + + const columns: any[] = [ + { index: 0, id: "version", label: "Version", width: 40, sortable: true, sortBy: VersionSortBy.version }, + { index: 1, id: "globalId", label: "Global Id", width: 10, sortable: true, sortBy: VersionSortBy.globalId }, + { index: 2, id: "contentId", label: "Content Id", width: 10, sortable: false }, + { index: 3, id: "createdOn", label: "Created on", width: 15, sortable: true, sortBy: VersionSortBy.createdOn }, + ]; + + const renderColumnData = (column: SearchedVersion, colIndex: number): React.ReactNode => { + // Name. + if (colIndex === 0) { + return ( +
+ + { column.version } + + ({column.name}) + + + +
+ ); + } + // Global id. + if (colIndex === 1) { + return ( + { column.globalId } + ); + } + // Global id. + if (colIndex === 2) { + return ( + { column.contentId } + ); + } + // Created on. + if (colIndex === 3) { + return ( + + ); + } + }; + + const actionsFor = (version: SearchedVersion): (VersionAction | VersionActionSeparator)[] => { + const vhash: number = shash(version.version); + // TODO hide/show actions based on user role + return [ + { label: "View version", onClick: () => props.onView(version), testId: `view-version-${vhash}` }, + { isSeparator: true }, + { label: "Delete version", onClick: () => props.onDelete(version), testId: `delete-version-${vhash}` } + ]; + }; + + const sortParams = (column: any): ThProps["sort"] | undefined => { + return column.sortable ? { + sortBy: { + index: sortByIndex, + direction: props.sortOrder + }, + onSort: (_event, index, direction) => { + props.onSort(columns[index].sortBy, direction === SortByDirection.asc ? SortOrder.asc : SortOrder.desc); + }, + columnIndex: column.index + } : undefined; + }; + + useEffect(() => { + if (props.sortBy === VersionSortBy.version) { + setSortByIndex(0); + } + if (props.sortBy === VersionSortBy.globalId) { + setSortByIndex(1); + } + if (props.sortBy === VersionSortBy.createdOn) { + setSortByIndex(3); + } + }, [props.sortBy]); + + return ( +
+ { + console.log(row); + }} + renderHeader={({ column, Th }) => ( + {column.label} + )} + renderCell={({ row, colIndex, Td }) => ( + + )} + renderActions={({ row }) => ( + item.label} + itemToTestId={item => item.testId} + itemIsDivider={item => item.isSeparator} + onSelect={item => item.onClick()} + testId={`api-actions-${shash(row.version)}`} + popperProps={{ + position: "right" + }} + /> + )} + /> +
+ ); +}; diff --git a/ui/ui-app/src/app/pages/artifact/components/tabs/index.ts b/ui/ui-app/src/app/pages/artifact/components/tabs/index.ts new file mode 100644 index 0000000000..f16f4dabb2 --- /dev/null +++ b/ui/ui-app/src/app/pages/artifact/components/tabs/index.ts @@ -0,0 +1,4 @@ +export * from "./ArtifactInfoTabContent"; +export * from "./VersionsTabContent"; +export * from "./VersionsTable"; +export * from "./VersionsTabToolbar"; diff --git a/ui/ui-app/src/app/pages/artifact/index.ts b/ui/ui-app/src/app/pages/artifact/index.ts index e3c86edaac..6d0ed122de 100644 --- a/ui/ui-app/src/app/pages/artifact/index.ts +++ b/ui/ui-app/src/app/pages/artifact/index.ts @@ -1 +1,2 @@ -export * from "./ArtifactRedirectPage.tsx"; \ No newline at end of file +export * from "./components"; +export * from "./ArtifactPage.tsx"; diff --git a/ui/ui-app/src/app/pages/artifactVersion/ArtifactVersionPage.tsx b/ui/ui-app/src/app/pages/artifactVersion/ArtifactVersionPage.tsx deleted file mode 100644 index dd8e050edb..0000000000 --- a/ui/ui-app/src/app/pages/artifactVersion/ArtifactVersionPage.tsx +++ /dev/null @@ -1,471 +0,0 @@ -import { FunctionComponent, useEffect, useState } from "react"; -import "./ArtifactVersionPage.css"; -import { - Breadcrumb, - BreadcrumbItem, - Button, - Modal, - PageSection, - PageSectionVariants, - Tab, - Tabs -} from "@patternfly/react-core"; -import { Link, useParams } from "react-router-dom"; -import { ArtifactMetaData } from "@models/artifactMetaData.model.ts"; -import { Rule } from "@models/rule.model.ts"; -import { SearchedVersion } from "@models/searchedVersion.model.ts"; -import { - ArtifactVersionPageHeader, - ContentTabContent, - DocumentationTabContent, - EditMetaDataModal, - InfoTabContent, - PageDataLoader, - PageError, - PageErrorHandler, - toPageError, - UploadVersionForm -} from "@app/pages"; -import { ReferencesTabContent } from "@app/pages/artifactVersion/components/tabs/ReferencesTabContent.tsx"; -import { IfFeature, InvalidContentModal } from "@app/components"; -import { ChangeOwnerModal } from "@app/pages/artifactVersion/components/modals/ChangeOwnerModal.tsx"; -import { ContentTypes } from "@models/contentTypes.model.ts"; -import { ApiError } from "@models/apiError.model.ts"; -import { PleaseWaitModal } from "@apicurio/common-ui-components"; -import { AppNavigation, useAppNavigation } from "@services/useAppNavigation.ts"; -import { LoggerService, useLoggerService } from "@services/useLoggerService.ts"; -import { CreateVersionData, EditableMetaData, GroupsService, useGroupsService } from "@services/useGroupsService.ts"; -import { DownloadService, useDownloadService } from "@services/useDownloadService.ts"; -import { ArtifactTypes } from "@services/useArtifactTypesService.ts"; -import { VersionMetaData } from "@models/versionMetaData.model.ts"; - - -export type ArtifactVersionPageProps = { - // No properties -} - -/** - * The artifact version page. - */ -export const ArtifactVersionPage: FunctionComponent = () => { - const [pageError, setPageError] = useState(); - const [loaders, setLoaders] = useState | Promise[] | undefined>(); - const [activeTabKey, setActiveTabKey] = useState("overview"); - const [artifact, setArtifact] = useState(); - const [artifactVersion, setArtifactVersion] = useState(); - const [artifactContent, setArtifactContent] = useState(""); - const [invalidContentError, setInvalidContentError] = useState(); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [isChangeOwnerModalOpen, setIsChangeOwnerModalOpen] = useState(false); - const [isInvalidContentModalOpen, setIsInvalidContentModalOpen] = useState(false); - const [isPleaseWaitModalOpen, setIsPleaseWaitModalOpen] = useState(false); - const [isUploadFormValid, setIsUploadFormValid] = useState(false); - const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); - const [pleaseWaitMessage, setPleaseWaitMessage] = useState(""); - const [rules, setRules] = useState([]); - const [uploadFormData, setUploadFormData] = useState(); - const [versions, setVersions] = useState([]); - - const appNavigation: AppNavigation = useAppNavigation(); - const logger: LoggerService = useLoggerService(); - const groups: GroupsService = useGroupsService(); - const download: DownloadService = useDownloadService(); - const { groupId, artifactId, version }= useParams(); - - const is404 = (e: any) => { - if (typeof e === "string") { - try { - const eo: any = JSON.parse(e); - if (eo && eo.error_code && eo.error_code === 404) { - return true; - } - } catch (e) { - // Do nothing - } - } - return false; - }; - - const createLoaders = (): Promise[] => { - let gid: string|null = groupId as string; - if (gid == "default") { - gid = null; - } - logger.info("Loading data for artifact: ", artifactId); - return [ - groups.getArtifactMetaData(gid, artifactId as string) - .then(setArtifact) - .catch(error => { - setPageError(toPageError(error, "Error loading page data.")); - }), - groups.getArtifactVersionMetaData(gid, artifactId as string, version as string) - .then(setArtifactVersion) - .catch(error => { - setPageError(toPageError(error, "Error loading page data.")); - }), - groups.getArtifactVersionContent(gid, artifactId as string, version as string) - .then(setArtifactContent) - .catch(e => { - logger.warn("Failed to get artifact content: ", e); - if (is404(e)) { - setArtifactContent("Artifact version content not available (404 Not Found)."); - } else { - const pageError: PageError = toPageError(e, "Error loading page data."); - setPageError(pageError); - } - }), - groups.getArtifactRules(gid, artifactId as string) - .then(setRules) - .catch(error => { - setPageError(toPageError(error, "Error loading page data.")); - }), - groups.getArtifactVersions(gid, artifactId as string) - .then(versions => { - setVersions(versions.reverse()); - }) - .catch(error => { - setPageError(toPageError(error, "Error loading page data.")); - }) - ]; - }; - - const handleTabClick = (_event: any, tabIndex: any): void => { - setActiveTabKey(tabIndex); - }; - - const onUploadVersion = (): void => { - setIsUploadModalOpen(true); - }; - - const onDeleteArtifact = (): void => { - setIsDeleteModalOpen(true); - }; - - const showDocumentationTab = (): boolean => { - return artifact?.type === "OPENAPI" && artifactVersion?.state !== "DISABLED"; - }; - - const doEnableRule = (ruleType: string): void => { - logger.debug("[ArtifactVersionPage] Enabling rule:", ruleType); - let config: string = "FULL"; - if (ruleType === "COMPATIBILITY") { - config = "BACKWARD"; - } - groups.createArtifactRule(groupId as string, artifactId as string, ruleType, config).catch(error => { - setPageError(toPageError(error, `Error enabling "${ ruleType }" artifact rule.`)); - }); - setRules([...rules, { config, type: ruleType }]); - }; - - const doDisableRule = (ruleType: string): void => { - logger.debug("[ArtifactVersionPage] Disabling rule:", ruleType); - groups.deleteArtifactRule(groupId as string, artifactId as string, ruleType).catch(error => { - setPageError(toPageError(error, `Error disabling "${ ruleType }" artifact rule.`)); - }); - setRules(rules.filter(r => r.type !== ruleType)); - }; - - const doConfigureRule = (ruleType: string, config: string): void => { - logger.debug("[ArtifactVersionPage] Configuring rule:", ruleType, config); - groups.updateArtifactRule(groupId as string, artifactId as string, ruleType, config).catch(error => { - setPageError(toPageError(error, `Error configuring "${ ruleType }" artifact rule.`)); - }); - setRules(rules.map(r => { - if (r.type === ruleType) { - return { config, type: r.type }; - } else { - return r; - } - })); - }; - - const doDownloadArtifact = (): void => { - const content: string = artifactContent; - - let contentType: string = ContentTypes.APPLICATION_JSON; - let fext: string = "json"; - if (artifact?.type === ArtifactTypes.PROTOBUF) { - contentType = ContentTypes.APPLICATION_PROTOBUF; - fext = "proto"; - } - if (artifact?.type === ArtifactTypes.WSDL) { - contentType = ContentTypes.APPLICATION_XML; - fext = "wsdl"; - } - if (artifact?.type === ArtifactTypes.XSD) { - contentType = ContentTypes.APPLICATION_XML; - fext = "xsd"; - } - if (artifact?.type === ArtifactTypes.XML) { - contentType = ContentTypes.APPLICATION_XML; - fext = "xml"; - } - if (artifact?.type === ArtifactTypes.GRAPHQL) { - contentType = ContentTypes.APPLICATION_JSON; - fext = "graphql"; - } - - const fname: string = nameOrId() + "." + fext; - download.downloadToFS(content, contentType, fname).catch(error => { - setPageError(toPageError(error, "Error downloading artifact content.")); - }); - }; - - const nameOrId = (): string => { - return artifact?.name || artifact?.artifactId || ""; - }; - - const artifactType = (): string => { - return artifact?.type || ""; - }; - - const versionName = (): string => { - return artifactVersion?.name || ""; - }; - - const versionDescription = (): string => { - return artifactVersion?.description || ""; - }; - - const versionLabels = (): { [key: string]: string } => { - return artifactVersion?.labels || {}; - }; - - const onUploadFormValid = (isValid: boolean): void => { - setIsUploadFormValid(isValid); - }; - - const onUploadFormChange = (data: string): void => { - setUploadFormData(data); - }; - - const onUploadModalClose = (): void => { - setIsUploadModalOpen(false); - }; - - const onDeleteModalClose = (): void => { - setIsDeleteModalOpen(false); - }; - - const doUploadArtifactVersion = (): void => { - onUploadModalClose(); - pleaseWait(true, "Uploading new version, please wait..."); - if (uploadFormData !== null) { - const data: CreateVersionData = { - content: uploadFormData as string, - type: artifactType() - }; - groups.createArtifactVersion(groupId as string, artifactId as string, data).then(versionMetaData => { - const groupId: string = versionMetaData.groupId ? versionMetaData.groupId : "default"; - const artifactVersionLocation: string = `/artifacts/${ encodeURIComponent(groupId) }/${ encodeURIComponent(versionMetaData.artifactId) }/versions/${versionMetaData.version}`; - logger.info("[ArtifactVersionPage] Artifact version successfully uploaded. Redirecting to details: ", artifactVersionLocation); - pleaseWait(false, ""); - appNavigation.navigateTo(artifactVersionLocation); - }).catch( error => { - pleaseWait(false, ""); - if (error && (error.error_code === 400 || error.error_code === 409)) { - handleInvalidContentError(error); - } else { - setPageError(toPageError(error, "Error uploading artifact version.")); - } - setUploadFormData(null); - setIsUploadFormValid(false); - }); - } - }; - - const doDeleteArtifact = (): void => { - onDeleteModalClose(); - pleaseWait(true, "Deleting artifact, please wait..."); - groups.deleteArtifact(groupId as string, artifactId as string).then( () => { - pleaseWait(false, ""); - appNavigation.navigateTo("/artifacts"); - }); - }; - - const openEditMetaDataModal = (): void => { - setIsEditModalOpen(true); - }; - - const openChangeOwnerModal = (): void => { - setIsChangeOwnerModalOpen(true); - }; - - const onEditModalClose = (): void => { - setIsEditModalOpen(false); - }; - - const onChangeOwnerModalClose = (): void => { - setIsChangeOwnerModalOpen(false); - }; - - const doEditMetaData = (metaData: EditableMetaData): void => { - groups.updateArtifactVersionMetaData(groupId as string, artifactId as string, version as string, metaData).then( () => { - if (artifact) { - setArtifactVersion({ - ...artifactVersion, - ...metaData - } as VersionMetaData); - } - }).catch( error => { - setPageError(toPageError(error, "Error editing artifact metadata.")); - }); - onEditModalClose(); - }; - - const doChangeOwner = (newOwner: string): void => { - groups.updateArtifactOwner(groupId as string, artifactId as string, newOwner).then( () => { - if (artifact) { - setArtifact({ - ...artifact, - owner: newOwner - }); - } - }).catch( error => { - setPageError(toPageError(error, "Error changing artifact ownership.")); - }); - onChangeOwnerModalClose(); - }; - - const closeInvalidContentModal = (): void => { - setIsInvalidContentModalOpen(false); - }; - - const pleaseWait = (isOpen: boolean, message: string): void => { - setIsPleaseWaitModalOpen(isOpen); - setPleaseWaitMessage(message); - }; - - const handleInvalidContentError = (error: any): void => { - logger.info("INVALID CONTENT ERROR", error); - setInvalidContentError(error); - setIsInvalidContentModalOpen(true); - }; - - useEffect(() => { - setLoaders(createLoaders()); - }, [groupId, artifactId, version]); - - const tabs: any[] = [ - - - , - - - , - - - , - - - , - ]; - if (!showDocumentationTab()) { - tabs.splice(1, 1); - } - - const gid: string = groupId || "default"; - const hasGroup: boolean = gid != "default"; - let breadcrumbs = ( - - Artifacts - { gid } - { artifactId as string } - - ); - if (!hasGroup) { - breadcrumbs = ( - - Artifacts - { artifactId as string } - - ); - } - - return ( - - - - - - - - - - - - - Upload, - - ]} - > - - - Delete, - - ]} - > -

Do you want to delete this artifact and all of its versions? This action cannot be undone.

-
- - - - -
- ); - -}; diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/index.ts b/ui/ui-app/src/app/pages/artifactVersion/components/index.ts deleted file mode 100644 index 0e2c19a9a7..0000000000 --- a/ui/ui-app/src/app/pages/artifactVersion/components/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./pageheader"; -export * from "./modals"; -export * from "./tabs"; -export * from "./uploadForm"; diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/modals/index.ts b/ui/ui-app/src/app/pages/artifactVersion/components/modals/index.ts deleted file mode 100644 index c143891031..0000000000 --- a/ui/ui-app/src/app/pages/artifactVersion/components/modals/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./EditMetaDataModal.tsx"; -export * from "./LabelsFormGroup.tsx"; - -export * from "./listToLabels.function.ts"; -export * from "./labelsToList.function.ts"; diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/modals/labelsToList.function.ts b/ui/ui-app/src/app/pages/artifactVersion/components/modals/labelsToList.function.ts deleted file mode 100644 index 2d52185f02..0000000000 --- a/ui/ui-app/src/app/pages/artifactVersion/components/modals/labelsToList.function.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ArtifactLabel } from "@app/pages"; - -export function labelsToList(labels: { [key: string]: string|undefined }): ArtifactLabel[] { - return Object.keys(labels).filter((key) => key !== undefined).map(key => { - return { - name: key, - value: labels[key], - nameValidated: "default", - valueValidated: "default" - }; - }); -} diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/modals/listToLabels.function.ts b/ui/ui-app/src/app/pages/artifactVersion/components/modals/listToLabels.function.ts deleted file mode 100644 index 81bc73990e..0000000000 --- a/ui/ui-app/src/app/pages/artifactVersion/components/modals/listToLabels.function.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ArtifactLabel } from "@app/pages"; - -export function listToLabels(labels: ArtifactLabel[]): { [key: string]: string|undefined } { - const rval: { [key: string]: string|undefined } = {}; - labels.forEach(label => { - if (label.name) { - rval[label.name] = label.value; - } - }); - return rval; -} diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/ArtifactVersionPageHeader.tsx b/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/ArtifactVersionPageHeader.tsx deleted file mode 100644 index c92fa9d352..0000000000 --- a/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/ArtifactVersionPageHeader.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { FunctionComponent } from "react"; -import "./ArtifactVersionPageHeader.css"; -import { Button, Flex, FlexItem, Text, TextContent, TextVariants } from "@patternfly/react-core"; -import { IfAuth, IfFeature } from "@app/components"; -import { VersionSelector } from "@app/pages"; -import { SearchedVersion } from "@models/searchedVersion.model.ts"; - - -/** - * Properties - */ -export type ArtifactVersionPageHeaderProps = { - title: string; - groupId: string; - artifactId: string; - onDeleteArtifact: () => void; - onUploadVersion: () => void; - version: string; - versions: SearchedVersion[]; -}; - -/** - * Models the page header for the Artifact page. - */ -export const ArtifactVersionPageHeader: FunctionComponent = (props: ArtifactVersionPageHeaderProps) => { - return ( - - - - { props.title } - - - - - - - - - - - - - ); -}; diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/VersionSelector.css b/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/VersionSelector.css deleted file mode 100644 index 70d494fb3b..0000000000 --- a/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/VersionSelector.css +++ /dev/null @@ -1,53 +0,0 @@ -.version-selector-dropdown .pf-v5-c-menu__content { - min-width: 300px; - max-width: 425px; -} - -.version-selector-dropdown.dropdown-align-right .pf-v5-c-menu__content { - right: 0; -} - -.version-selector-dropdown .version-filter { - padding-left: 5px; - padding-right: 5px; -} - -.version-selector-dropdown .version-list, .version-selector-dropdown .version-header { - margin-top: 5px; - padding-left: 5px; - padding-right: 1px; - margin-right: 5px; -} -.version-selector-dropdown .version-list { - max-height: 400px; - overflow-y: scroll; -} - -.version-selector-dropdown .version-list .version-item, .version-selector-dropdown .version-header .version-item { - display: flex; - flex-direction: row; - padding: 3px; - width: 100%; - color: inherit; -} -.version-selector-dropdown .version-list .version-item:hover { - background-color: #eee; - cursor: pointer; - text-decoration: none; -} - -.version-selector-dropdown .version-list .version-item .name, .version-selector-dropdown .version-header .version-item .name { - flex-grow: 1; -} -.version-selector-dropdown .version-list .version-item .date, .version-selector-dropdown .version-header .version-item .date { - flex-grow: 1; - text-align: right; -} - -.version-selector-dropdown .version-header .version-item { - border-bottom: 1px solid #ccc; -} -.version-selector-dropdown .version-header .version-item .name, .version-selector-dropdown .version-header .version-item .date { - font-weight: bold; - padding-right: 18px; -} \ No newline at end of file diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/VersionSelector.tsx b/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/VersionSelector.tsx deleted file mode 100644 index 1167dbf0b5..0000000000 --- a/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/VersionSelector.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { FunctionComponent, useState } from "react"; -import "./VersionSelector.css"; -import { - Button, - ButtonVariant, - Dropdown, - InputGroup, - MenuToggle, - MenuToggleElement, - TextInput -} from "@patternfly/react-core"; -import { SearchIcon } from "@patternfly/react-icons"; -import { Link } from "react-router-dom"; -import { SearchedVersion } from "@models/searchedVersion.model.ts"; -import { FromNow } from "@apicurio/common-ui-components"; -import { AppNavigation, useAppNavigation } from "@services/useAppNavigation.ts"; -import { ConfigService, useConfigService } from "@services/useConfigService.ts"; - - -/** - * Properties - */ -export type VersionSelectorProps = { - groupId: string; - artifactId: string; - version: string; - versions: SearchedVersion[]; -} - -export const VersionSelector: FunctionComponent = (props: VersionSelectorProps) => { - const [isOpen, setOpen] = useState(false); - - const config: ConfigService = useConfigService(); - const appNav: AppNavigation = useAppNavigation(); - - const dropdownClasses = (): string => { - const classes: string[] = [ "version-selector-dropdown" ]; - if (config.featureReadOnly()) { - classes.push("dropdown-align-right"); - } - return classes.join(" "); - }; - - const onToggle = (): void => { - setOpen(!isOpen); - }; - - return ( - ) => ( - - Version: { props.version } - - )} - isOpen={isOpen} - > -
- - - - -
-
-
- Version - Created On -
-
-
- - latest - - - { - props.versions.map((v, idx) => - - { v.version } - - - - - ) - } -
-
- ); -}; diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/index.ts b/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/index.ts deleted file mode 100644 index 9bb2124a98..0000000000 --- a/ui/ui-app/src/app/pages/artifactVersion/components/pageheader/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./ArtifactVersionPageHeader.tsx"; -export * from "./VersionSelector.tsx"; diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/uploadForm/UploadVersionForm.tsx b/ui/ui-app/src/app/pages/artifactVersion/components/uploadForm/UploadVersionForm.tsx deleted file mode 100644 index dbd6c4f541..0000000000 --- a/ui/ui-app/src/app/pages/artifactVersion/components/uploadForm/UploadVersionForm.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { FunctionComponent, useEffect, useState } from "react"; -import { FileUpload, Form, FormGroup } from "@patternfly/react-core"; - - -/** - * Properties - */ -export type UploadVersionFormProps = { - onValid: (valid: boolean) => void; - onChange: (data: string) => void; -}; - -export const UploadVersionForm: FunctionComponent = (props: UploadVersionFormProps) => { - const [content, setContent] = useState(""); - const [contentFilename] = useState(""); - const [contentIsLoading, setContentIsLoading] = useState(false); - const [valid, setValid] = useState(false); - - const onContentChange = (_event: any, value: any): void => { - setContent(value); - }; - - const onFileReadStarted = (): void => { - setContentIsLoading(true); - }; - - const onFileReadFinished = (): void => { - setContentIsLoading(false); - }; - - const checkValid = (): void => { - const data: string = currentData(); - const newValid: boolean = isValid(data); - setValid(newValid); - }; - - const isValid = (data: string): boolean => { - return !!data; - }; - - const currentData = (): string => { - return content; - }; - - const fireOnChange = (): void => { - if (props.onChange) { - props.onChange(currentData()); - } - }; - - const fireOnValid = (): void => { - if (props.onValid) { - props.onValid(valid); - } - }; - - useEffect(() => { - fireOnValid(); - }, [valid]); - - useEffect(() => { - fireOnChange(); - checkValid(); - }, [content]); - - return ( - - - onContentChange({}, "")} - isLoading={contentIsLoading} - /> - - - ); -}; - diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/uploadForm/index.ts b/ui/ui-app/src/app/pages/artifactVersion/components/uploadForm/index.ts deleted file mode 100644 index fc567995e9..0000000000 --- a/ui/ui-app/src/app/pages/artifactVersion/components/uploadForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./UploadVersionForm.tsx"; \ No newline at end of file diff --git a/ui/ui-app/src/app/pages/artifactVersion/index.ts b/ui/ui-app/src/app/pages/artifactVersion/index.ts deleted file mode 100644 index a5d1a89aca..0000000000 --- a/ui/ui-app/src/app/pages/artifactVersion/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./components"; -export * from "./ArtifactVersionPage.tsx"; diff --git a/ui/ui-app/src/app/pages/artifacts/ArtifactsPage.tsx b/ui/ui-app/src/app/pages/artifacts/ArtifactsPage.tsx deleted file mode 100644 index 1a046150a8..0000000000 --- a/ui/ui-app/src/app/pages/artifacts/ArtifactsPage.tsx +++ /dev/null @@ -1,415 +0,0 @@ -import { FunctionComponent, useEffect, useState } from "react"; -import "./ArtifactsPage.css"; -import { - Button, - FileUpload, - Flex, - FlexItem, - Form, - FormGroup, - FormHelperText, - HelperText, - HelperTextItem, - Modal, - PageSection, - PageSectionVariants, - Spinner -} from "@patternfly/react-core"; -import { - ArtifactList, - ArtifactsPageEmptyState, - ArtifactsPageToolbar, - ArtifactsPageToolbarFilterCriteria, - PageDataLoader, - PageError, - PageErrorHandler, - toPageError, - UploadArtifactForm -} from "@app/pages"; -import { InvalidContentModal, RootPageHeader } from "@app/components"; -import { ApiError } from "@models/apiError.model.ts"; -import { SearchedArtifact } from "@models/searchedArtifact.model.ts"; -import { useSearchParams } from "react-router-dom"; -import { If, PleaseWaitModal, ProgressModal } from "@apicurio/common-ui-components"; -import { - ArtifactsSearchResults, - CreateArtifactData, - GetArtifactsCriteria, - Paging, - useGroupsService -} from "@services/useGroupsService.ts"; -import { AppNavigation, useAppNavigation } from "@services/useAppNavigation.ts"; -import { useAdminService } from "@services/useAdminService.ts"; -import { useLoggerService } from "@services/useLoggerService.ts"; - -/** - * Properties - */ -export type ArtifactsPageProps = { - // No properties. -} - -const EMPTY_UPLOAD_FORM_DATA: CreateArtifactData = { - content: undefined, fromURL: undefined, groupId: "", id: null, sha: undefined, type: "" -}; -const EMPTY_RESULTS: ArtifactsSearchResults = { - artifacts: [], - count: 0, - page: 1, - pageSize: 10 -}; - -type HookFunctionWrapper = { - hook: any; -}; - -const DEFAULT_PAGING: Paging = { - page: 1, - pageSize: 10 -}; - -/** - * The artifacts page. - */ -//class ArtifactsPageInternal extends PageComponent { -export const ArtifactsPage: FunctionComponent = () => { - const [pageError, setPageError] = useState(); - const [loaders, setLoaders] = useState | Promise[] | undefined>(); - const [criteria, setCriteria] = useState({ - filterSelection: "name", - filterValue: "", - ascending: true - }); - const [isUploadModalOpen, setUploadModalOpen] = useState(false); - const [isImportModalOpen, setImportModalOpen] = useState(false); - const [isUploadFormValid, setUploadFormValid] = useState(false); - const [isImportFormValid, setImportFormValid] = useState(false); - const [isInvalidContentModalOpen, setInvalidContentModalOpen] = useState(false); - const [isPleaseWaitModalOpen, setPleaseWaitModalOpen] = useState(false); - const [isSearching, setSearching] = useState(false); - const [paging, setPaging] = useState(DEFAULT_PAGING); - const [results, setResults] = useState(EMPTY_RESULTS); - const [uploadFormData, setUploadFormData] = useState(EMPTY_UPLOAD_FORM_DATA); - const [invalidContentError, setInvalidContentError] = useState(); - const [importFilename, setImportFilename] = useState(""); - const [importFile, setImportFile] = useState(); - const [isImporting, setImporting] = useState(false); - const [importProgress, setImportProgress] = useState(0); - const [filterByGroupHook, setFilterByGroupHook] = useState(); - - const appNavigation: AppNavigation = useAppNavigation(); - const admin = useAdminService(); - const groups = useGroupsService(); - const logger = useLoggerService(); - const [ searchParams ] = useSearchParams(); - - useEffect(() => { - if (filterByGroupHook && searchParams.get("group")) { - filterByGroupHook?.hook(searchParams.get("group")); - } - }, [filterByGroupHook]); - - const createLoaders = (): Promise => { - return search(criteria, paging); - }; - - const onUploadArtifact = (): void => { - setUploadModalOpen(true); - }; - - const onImportArtifacts = (): void => { - setImportModalOpen(true); - }; - - const onExportArtifacts = (): void => { - admin.exportAs("all-artifacts.zip").then(dref => { - const link = document.createElement("a"); - link.href = dref.href; - link.download = "all-artifacts.zip"; - link.click(); - }).catch(error => { - setPageError(toPageError(error, "Failed to export artifacts")); - }); - }; - - const onUploadModalClose = (): void => { - setUploadModalOpen(false); - }; - - const onImportModalClose = (): void => { - setImportModalOpen(false); - }; - - const onArtifactsLoaded = (results: ArtifactsSearchResults): void => { - setSearching(false); - setResults(results); - }; - - const doImport = (): void => { - setImporting(true); - setImportProgress(0); - setImportModalOpen(false); - - // Reset the import dialog. - setImportFilename(""); - setImportFile(undefined); - setImportFormValid(false); - - if (importFile != null) { - admin.importFrom(importFile, (event: any) => { - let progress: number = 0; - if (event.lengthComputable) { - progress = Math.round(100 * (event.loaded / event.total)); - } - setImportProgress(progress); - }).then(() => { - setTimeout(() => { - setImporting(false); - setImportProgress(100); - setImportModalOpen(false); - search(criteria, paging); - }, 1500); - }).catch(error => { - setPageError(toPageError(error, "Error importing multiple artifacts")); - }); - } - }; - - const doUploadArtifact = (): void => { - onUploadModalClose(); - pleaseWait(true); - - if (uploadFormData !== null) { - const data: CreateArtifactData = { - ...uploadFormData - }; - // If no groupId is provided, set it to the "default" group - if (!uploadFormData.groupId) { - data.groupId = "default"; - } - groups.createArtifact(data).then(response => { - const groupId: string = response.artifact.groupId ? response.artifact.groupId : "default"; - const artifactLocation: string = `/artifacts/${ encodeURIComponent(groupId) }/${ encodeURIComponent(response.artifact.artifactId) }`; - logger.info("[ArtifactsPage] Artifact successfully uploaded. Redirecting to details: ", artifactLocation); - appNavigation.navigateTo(artifactLocation); - }).catch( error => { - pleaseWait(false); - if (error && (error.error_code === 400 || error.error_code === 409)) { - handleInvalidContentError(error); - } else { - setPageError(toPageError(error, "Error uploading artifact.")); - } - setUploadFormData(EMPTY_UPLOAD_FORM_DATA); - setUploadFormValid(false); - }); - } - }; - - const artifacts = (): SearchedArtifact[] => { - return results ? results.artifacts : []; - }; - - const artifactsCount = (): number => { - return results ? results.artifacts.length : 0; - }; - - const onFilterCriteriaChange = (newCriteria: ArtifactsPageToolbarFilterCriteria): void => { - setCriteria(newCriteria); - search(newCriteria, paging); - }; - - const isFiltered = (): boolean => { - return !!criteria.filterValue; - }; - - const search = async (criteria: ArtifactsPageToolbarFilterCriteria, paging: Paging): Promise => { - setSearching(true); - const gac: GetArtifactsCriteria = { - sortAscending: criteria.ascending, - type: criteria.filterSelection, - value: criteria.filterValue - }; - return groups.getArtifacts(gac, paging).then(results => { - onArtifactsLoaded(results); - }).catch(error => { - setPageError(toPageError(error, "Error searching for artifacts.")); - }); - }; - - const onSetPage = (_event: any, newPage: number, perPage?: number): void => { - const newPaging: Paging = { - page: newPage, - pageSize: perPage ? perPage : paging.pageSize - }; - setPaging(newPaging); - search(criteria, newPaging); - }; - - const onPerPageSelect = (_event: any, newPerPage: number): void => { - const newPaging: Paging = { - page: paging.page, - pageSize: newPerPage - }; - setPaging(newPaging); - search(criteria, newPaging); - }; - - const onUploadFormValid = (isValid: boolean): void => { - setUploadFormValid(isValid); - }; - - const onUploadFormChange = (data: CreateArtifactData): void => { - setUploadFormData(data); - }; - - const onImportFileChange = (_event: any, file: File): void => { - const filename: string = file.name; - const isValid: boolean = filename.toLowerCase().endsWith(".zip"); - setImportFilename(filename); - setImportFile(file); - setImportFormValid(isValid); - }; - - const closeInvalidContentModal = (): void => { - setInvalidContentModalOpen(false); - }; - - const pleaseWait = (isOpen: boolean): void => { - setPleaseWaitModalOpen(isOpen); - }; - - const handleInvalidContentError = (error: any): void => { - logger.info("[ArtifactsPage] Invalid content error:", error); - setInvalidContentError(error); - setInvalidContentModalOpen(true); - }; - - const onGroupClick = (groupId: string): void => { - logger.info("[ArtifactsPage] Filtering by group: ", groupId); - // Hack Alert: when clicking on a Group in the artifact list, push a new filter state into - // the toolbar. This is done via a change-criteria hook function that we set up earlier. - if (filterByGroupHook) { - filterByGroupHook.hook(groupId); - } - - // Also reset paging. - setPaging(DEFAULT_PAGING); - }; - - const showToolbar = (): boolean => { - // TODO only show when not loading content? - return true; - }; - - useEffect(() => { - if (searchParams.get("group")) { - setLoaders([]); - } else { - setLoaders(createLoaders()); - } - }, []); - - return ( - - - - - - - - setFilterByGroupHook({ hook })} - paging={paging} - onPerPageSelect={onPerPageSelect} - onSetPage={onSetPage} - onUploadArtifact={onUploadArtifact} - onExportArtifacts={onExportArtifacts} - onImportArtifacts={onImportArtifacts} - onCriteriaChange={onFilterCriteriaChange} /> - - - - { - isSearching ? - - - Searching... - - : - artifactsCount() === 0 ? - - : - - } - - - Upload, - - ]} - > - - - - Upload, - - ]} - > -
- -

- Select an artifacts .zip file previously downloaded from a Registry instance. -

-
- - - - - File format must be .zip - - - -
-
- - setImporting(false)} - isOpen={isImporting} /> -
- ); - -}; diff --git a/ui/ui-app/src/app/pages/artifacts/components/artifactList/ArtifactGroup.tsx b/ui/ui-app/src/app/pages/artifacts/components/artifactList/ArtifactGroup.tsx deleted file mode 100644 index f2302f3a56..0000000000 --- a/ui/ui-app/src/app/pages/artifacts/components/artifactList/ArtifactGroup.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { FunctionComponent } from "react"; -import "./ArtifactList.css"; - -/** - * Properties - */ -export type ArtifactGroupProps = { - groupId: string|null; - onClick: (groupId: string) => void; -}; - - -/** - * Models the list of artifacts. - */ -export const ArtifactGroup: FunctionComponent = (props: ArtifactGroupProps) => { - - const style = (): string => { - return !props.groupId ? "nogroup" : "group"; - }; - - const fireOnClick = (): void => { - props.onClick(props.groupId as string); - }; - - return ( - {props.groupId} - ); - -}; diff --git a/ui/ui-app/src/app/pages/artifacts/components/empty/ArtifactsPageEmptyState.tsx b/ui/ui-app/src/app/pages/artifacts/components/empty/ArtifactsPageEmptyState.tsx deleted file mode 100644 index 80cb057368..0000000000 --- a/ui/ui-app/src/app/pages/artifacts/components/empty/ArtifactsPageEmptyState.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { FunctionComponent } from "react"; -import "./ArtifactsPageEmptyState.css"; -import { - Button, - EmptyState, - EmptyStateActions, - EmptyStateBody, - EmptyStateFooter, - EmptyStateIcon, - EmptyStateVariant, - Title -} from "@patternfly/react-core"; -import { PlusCircleIcon } from "@patternfly/react-icons"; -import { IfAuth, IfFeature } from "@app/components"; -import { If } from "@apicurio/common-ui-components"; - -/** - * Properties - */ -export type ArtifactsPageEmptyStateProps = { - isFiltered: boolean; - onUploadArtifact: () => void; - onImportArtifacts: () => void; -}; - - -/** - * Models the empty state for the Artifacts page (when there are no artifacts). - */ -export const ArtifactsPageEmptyState: FunctionComponent = (props: ArtifactsPageEmptyStateProps) => { - return ( - - - - No artifacts found - - props.isFiltered}> - - No artifacts match your filter settings. Change your filter or perhaps Upload a new - artifact. - - - !props.isFiltered}> - - There are currently no artifacts in the registry. Upload artifacts to view them here. - - - - - - - - - - - - - - - - - - ); - -}; diff --git a/ui/ui-app/src/app/pages/artifacts/components/empty/index.ts b/ui/ui-app/src/app/pages/artifacts/components/empty/index.ts deleted file mode 100644 index eda54cb11f..0000000000 --- a/ui/ui-app/src/app/pages/artifacts/components/empty/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ArtifactsPageEmptyState.tsx"; diff --git a/ui/ui-app/src/app/pages/artifacts/components/toolbar/ArtifactsPageToolbar.tsx b/ui/ui-app/src/app/pages/artifacts/components/toolbar/ArtifactsPageToolbar.tsx deleted file mode 100644 index c040b3c2b9..0000000000 --- a/ui/ui-app/src/app/pages/artifacts/components/toolbar/ArtifactsPageToolbar.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { FunctionComponent, useEffect, useState } from "react"; -import "./ArtifactsPageToolbar.css"; -import { - Button, - ButtonVariant, - Form, - InputGroup, - Pagination, - TextInput, - Toolbar, - ToolbarContent, - ToolbarItem -} from "@patternfly/react-core"; -import { SearchIcon, SortAlphaDownAltIcon, SortAlphaDownIcon } from "@patternfly/react-icons"; -import { IfAuth, IfFeature } from "@app/components"; -import { OnPerPageSelect, OnSetPage } from "@patternfly/react-core/dist/js/components/Pagination/Pagination"; -import { ObjectDropdown, ObjectSelect } from "@apicurio/common-ui-components"; -import { ArtifactsSearchResults, Paging } from "@services/useGroupsService.ts"; -import { useLoggerService } from "@services/useLoggerService.ts"; - -export type ArtifactsPageToolbarFilterCriteria = { - filterSelection: string; - filterValue: string; - ascending: boolean; -}; - -export type FilterByGroupFunction = (groupId: string) => void; - -export type ArtifactsPageToolbarProps = { - artifacts: ArtifactsSearchResults; - onCriteriaChange: (criteria: ArtifactsPageToolbarFilterCriteria) => void; - criteria: ArtifactsPageToolbarFilterCriteria; - paging: Paging; - onPerPageSelect: OnPerPageSelect; - onSetPage: OnSetPage; - onUploadArtifact: () => void; - onImportArtifacts: () => void; - onExportArtifacts: () => void; - filterByGroupHook: (hook: FilterByGroupFunction) => void; -}; - -type FilterType = { - value: string; - label: string; - testId: string; -}; -const FILTER_TYPES: FilterType[] = [ - { value: "name", label: "Name", testId: "artifact-filter-typename" }, - { value: "group", label: "Group", testId: "artifact-filter-typegroup" }, - { value: "description", label: "Description", testId: "artifact-filter-typedescription" }, - { value: "labels", label: "Labels", testId: "artifact-filter-typelabels" }, - { value: "globalId", label: "Global Id", testId: "artifact-filter-typeglobal-id" }, - { value: "contentId", label: "Content Id", testId: "artifact-filter-typecontent-id" }, -]; -const DEFAULT_FILTER_TYPE = FILTER_TYPES[0]; - - -type ActionType = { - label: string; - callback: () => void; -}; - -/** - * Models the toolbar for the Artifacts page. - */ -export const ArtifactsPageToolbar: FunctionComponent = (props: ArtifactsPageToolbarProps) => { - const [filterType, setFilterType] = useState(DEFAULT_FILTER_TYPE); - const [filterValue, setFilterValue] = useState(""); - const [filterAscending, setFilterAscending] = useState(true); - const [kebabActions, setKebabActions] = useState([]); - - const logger = useLoggerService(); - - const totalArtifactsCount = (): number => { - return props.artifacts ? props.artifacts.count : 0; - }; - - const onFilterSubmit = (event: any|undefined): void => { - fireChangeEvent(filterAscending, filterType.value, filterValue); - if (event) { - event.preventDefault(); - } - }; - - const onFilterTypeChange = (newType: FilterType): void => { - setFilterType(newType); - fireChangeEvent(filterAscending, newType.value, filterValue); - }; - - const onToggleAscending = (): void => { - logger.debug("[ArtifactsPageToolbar] Toggle the ascending flag."); - const newAscending: boolean = !filterAscending; - setFilterAscending(newAscending); - fireChangeEvent(newAscending, filterType.value, filterValue); - }; - - const fireChangeEvent = (ascending: boolean, filterSelection: string, filterValue: string): void => { - const criteria: ArtifactsPageToolbarFilterCriteria = { - ascending, - filterSelection, - filterValue - }; - props.onCriteriaChange(criteria); - }; - - const filterByGroup = (groupId: string): void => { - logger.info("[ArtifactsPageToolbar] Filtering by group: ", groupId); - if (groupId) { - const newFilterType: FilterType = FILTER_TYPES[1]; // Filter by group - const newFilterValue: string = groupId; - setFilterType(newFilterType); - setFilterValue(newFilterValue); - fireChangeEvent(filterAscending, newFilterType.value, newFilterValue); - } - }; - - useEffect(() => { - if (props.filterByGroupHook) { - logger.info("[ArtifactsPageToolbar] Setting change criteria hook"); - props.filterByGroupHook(filterByGroup); - } - }, []); - - useEffect(() => { - const adminActions: ActionType[] = [ - { label: "Upload multiple artifacts", callback: props.onImportArtifacts }, - { label: "Download all artifacts (.zip file)", callback: props.onExportArtifacts } - ]; - setKebabActions(adminActions); - }, [props.onExportArtifacts, props.onImportArtifacts]); - - return ( - - - -
- - item.testId} - itemToString={(item) => item.label} /> - setFilterValue(value)} - data-testid="artifact-filter-value" - aria-label="search input example"/> - - -
-
- - - - - - - - - - - - - item.callback()} - itemToString={(item) => item.label} - isKebab={true} /> - - - - - -
-
- ); - -}; diff --git a/ui/ui-app/src/app/pages/artifacts/components/toolbar/index.ts b/ui/ui-app/src/app/pages/artifacts/components/toolbar/index.ts deleted file mode 100644 index 99d2fef7b7..0000000000 --- a/ui/ui-app/src/app/pages/artifacts/components/toolbar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ArtifactsPageToolbar.tsx"; diff --git a/ui/ui-app/src/app/pages/artifacts/components/uploadForm/index.ts b/ui/ui-app/src/app/pages/artifacts/components/uploadForm/index.ts deleted file mode 100644 index 8be9f98d1b..0000000000 --- a/ui/ui-app/src/app/pages/artifacts/components/uploadForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./UploadArtifactForm.tsx"; \ No newline at end of file diff --git a/ui/ui-app/src/app/pages/artifacts/index.ts b/ui/ui-app/src/app/pages/artifacts/index.ts deleted file mode 100644 index 3130844e7f..0000000000 --- a/ui/ui-app/src/app/pages/artifacts/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./ArtifactsPage.tsx"; -export * from "./components"; \ No newline at end of file diff --git a/ui/ui-app/src/app/pages/artifacts/ArtifactsPage.css b/ui/ui-app/src/app/pages/explore/ExplorePage.css similarity index 100% rename from ui/ui-app/src/app/pages/artifacts/ArtifactsPage.css rename to ui/ui-app/src/app/pages/explore/ExplorePage.css diff --git a/ui/ui-app/src/app/pages/explore/ExplorePage.tsx b/ui/ui-app/src/app/pages/explore/ExplorePage.tsx new file mode 100644 index 0000000000..8a45131c2d --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/ExplorePage.tsx @@ -0,0 +1,355 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import "./ExplorePage.css"; +import { PageSection, PageSectionVariants, TextContent } from "@patternfly/react-core"; +import { + ArtifactList, + ExplorePageEmptyState, + ExplorePageToolbar, + ExplorePageToolbarFilterCriteria, + GroupList, + ImportModal, + PageDataLoader, + PageError, + PageErrorHandler, + toPageError +} from "@app/pages"; +import { CreateArtifactModal, CreateGroupModal, InvalidContentModal, RootPageHeader } from "@app/components"; +import { ApiError } from "@models/apiError.model.ts"; +import { If, ListWithToolbar, PleaseWaitModal, ProgressModal } from "@apicurio/common-ui-components"; +import { useGroupsService } from "@services/useGroupsService.ts"; +import { AppNavigation, useAppNavigation } from "@services/useAppNavigation.ts"; +import { useAdminService } from "@services/useAdminService.ts"; +import { useLoggerService } from "@services/useLoggerService.ts"; +import { ExploreType } from "@app/pages/explore/ExploreType.ts"; +import { ArtifactSearchResults } from "@models/artifactSearchResults.model.ts"; +import { Paging } from "@models/paging.model.ts"; +import { FilterBy, SearchFilter, useSearchService } from "@services/useSearchService.ts"; +import { GroupSearchResults } from "@models/groupSearchResults.model.ts"; +import { CreateGroup } from "@models/createGroup.model.ts"; +import { SortOrder } from "@models/sortOrder.model.ts"; +import { ArtifactSortBy } from "@models/artifactSortBy.model.ts"; +import { GroupSortBy } from "@models/groupSortBy.model.ts"; +import { CreateArtifact } from "@models/createArtifact.model.ts"; + +/** + * Properties + */ +export type ExplorePageProps = { + // No properties. +} + +const EMPTY_RESULTS: ArtifactSearchResults = { + artifacts: [], + count: 0 +}; + +const DEFAULT_PAGING: Paging = { + page: 1, + pageSize: 10 +}; + +/** + * The Explore page. + */ +export const ExplorePage: FunctionComponent = () => { + const [pageError, setPageError] = useState(); + const [loaders, setLoaders] = useState | Promise[] | undefined>(); + const [exploreType, setExploreType] = useState(ExploreType.ARTIFACT); + const [criteria, setCriteria] = useState({ + filterBy: FilterBy.name, + filterValue: "", + ascending: true + }); + const [isCreateArtifactModalOpen, setCreateArtifactModalOpen] = useState(false); + const [isCreateGroupModalOpen, setCreateGroupModalOpen] = useState(false); + const [isImportModalOpen, setImportModalOpen] = useState(false); + const [isInvalidContentModalOpen, setInvalidContentModalOpen] = useState(false); + const [isPleaseWaitModalOpen, setPleaseWaitModalOpen] = useState(false); + const [pleaseWaitMessage, setPleaseWaitMessage] = useState(""); + const [isSearching, setSearching] = useState(false); + const [isImporting, setImporting] = useState(false); + const [paging, setPaging] = useState(DEFAULT_PAGING); + const [results, setResults] = useState(EMPTY_RESULTS); + const [invalidContentError, setInvalidContentError] = useState(); + const [importProgress, setImportProgress] = useState(0); + + const appNavigation: AppNavigation = useAppNavigation(); + const admin = useAdminService(); + const searchSvc = useSearchService(); + const groups = useGroupsService(); + const logger = useLoggerService(); + + const createLoaders = (): Promise => { + return search(exploreType, criteria, paging); + }; + + const onCreateGroup = (): void => { + setCreateGroupModalOpen(true); + }; + + const onCreateArtifact = (): void => { + setCreateArtifactModalOpen(true); + }; + + const onImportArtifacts = (): void => { + setImportModalOpen(true); + }; + + const onExportArtifacts = (): void => { + admin.exportAs("all-artifacts.zip").then(dref => { + const link = document.createElement("a"); + link.href = dref.href; + link.download = "all-artifacts.zip"; + link.click(); + }).catch(error => { + setPageError(toPageError(error, "Failed to export artifacts")); + }); + }; + + const onCreateArtifactModalClose = (): void => { + setCreateArtifactModalOpen(false); + }; + + const onImportModalClose = (): void => { + setImportModalOpen(false); + }; + + const onResultsLoaded = (results: ArtifactSearchResults | GroupSearchResults): void => { + setSearching(false); + setResults(results); + }; + + const doImport = (file: File | undefined): void => { + setImporting(true); + setImportProgress(0); + setImportModalOpen(false); + + if (file != null) { + admin.importFrom(file, (event: any) => { + let progress: number = 0; + if (event.lengthComputable) { + progress = Math.round(100 * (event.loaded / event.total)); + } + setImportProgress(progress); + }).then(() => { + setTimeout(() => { + setImporting(false); + setImportProgress(100); + setImportModalOpen(false); + search(exploreType, criteria, paging); + }, 1500); + }).catch(error => { + setPageError(toPageError(error, "Error importing multiple artifacts")); + }); + } + }; + + const doCreateArtifact = (groupId: string|null, data: CreateArtifact): void => { + onCreateArtifactModalClose(); + pleaseWait(true); + + if (data !== null) { + groups.createArtifact(groupId, data).then(response => { + const groupId: string = response.artifact.groupId || "default"; + const artifactLocation: string = `/explore/${ encodeURIComponent(groupId) }/${ encodeURIComponent(response.artifact.artifactId) }`; + logger.info("[ExplorePage] Artifact successfully created. Redirecting to details: ", artifactLocation); + appNavigation.navigateTo(artifactLocation); + }).catch( error => { + pleaseWait(false); + if (error && (error.error_code === 400 || error.error_code === 409)) { + handleInvalidContentError(error); + } else { + setPageError(toPageError(error, "Error creating artifact.")); + } + }); + } + }; + + const doCreateGroup = (data: CreateGroup): void => { + setCreateGroupModalOpen(false); + pleaseWait(true); + + groups.createGroup(data).then(response => { + const groupId: string = response.groupId; + const groupLocation: string = `/explore/${ encodeURIComponent(groupId) }`; + logger.info("[ExplorePage] Group successfully created. Redirecting to details page: ", groupLocation); + appNavigation.navigateTo(groupLocation); + }).catch( error => { + pleaseWait(false); + if (error && (error.error_code === 400 || error.error_code === 409)) { + handleInvalidContentError(error); + } else { + setPageError(toPageError(error, "Error creating group.")); + } + }); + }; + + const onFilterCriteriaChange = (newCriteria: ExplorePageToolbarFilterCriteria): void => { + setCriteria(newCriteria); + search(exploreType, newCriteria, paging); + }; + + const isFiltered = (): boolean => { + return !!criteria.filterValue; + }; + + const search = async (exploreType: ExploreType, criteria: ExplorePageToolbarFilterCriteria, paging: Paging): Promise => { + setSearching(true); + const filters: SearchFilter[] = [ + { + by: criteria.filterBy, + value: criteria.filterValue + } + ]; + + const sortOrder: SortOrder = criteria.ascending ? SortOrder.asc : SortOrder.desc; + if (exploreType === ExploreType.ARTIFACT) { + return searchSvc.searchArtifacts(filters, ArtifactSortBy.name, sortOrder, paging).then(results => { + onResultsLoaded(results); + }).catch(error => { + setPageError(toPageError(error, "Error searching for artifacts.")); + }); + } else if (exploreType === ExploreType.GROUP) { + return searchSvc.searchGroups(filters, GroupSortBy.groupId, sortOrder, paging).then(results => { + onResultsLoaded(results); + }).catch(error => { + setPageError(toPageError(error, "Error searching for groups.")); + }); + } + }; + + const onSetPage = (_event: any, newPage: number, perPage?: number): void => { + const newPaging: Paging = { + page: newPage, + pageSize: perPage ? perPage : paging.pageSize + }; + setPaging(newPaging); + search(exploreType, criteria, newPaging); + }; + + const onPerPageSelect = (_event: any, newPerPage: number): void => { + const newPaging: Paging = { + page: paging.page, + pageSize: newPerPage + }; + setPaging(newPaging); + search(exploreType, criteria, newPaging); + }; + + const onExploreTypeChange = (newExploreType: ExploreType): void => { + const newCriteria: ExplorePageToolbarFilterCriteria = { + filterBy: FilterBy.name, + filterValue: "", + ascending: true + }; + const newPaging: Paging = DEFAULT_PAGING; + + setPaging(newPaging); + setCriteria(newCriteria); + setExploreType(newExploreType); + + search(newExploreType, newCriteria, newPaging); + }; + + const closeInvalidContentModal = (): void => { + setInvalidContentModalOpen(false); + }; + + const pleaseWait = (isOpen: boolean, message: string = ""): void => { + setPleaseWaitModalOpen(isOpen); + setPleaseWaitMessage(message); + }; + + const handleInvalidContentError = (error: any): void => { + logger.info("[ExplorePage] Invalid content error:", error); + setInvalidContentError(error); + setInvalidContentModalOpen(true); + }; + + useEffect(() => { + setLoaders(createLoaders()); + }, []); + + const toolbar = ( + + ); + + const emptyState = ( + + ); + + return ( + + + + + + + + Explore content in the registry by searching for groups or artifacts. + + + + + + + + + + + + + + + setCreateGroupModalOpen(false)} + onCreate={doCreateGroup} /> + + + + setImporting(false)} + isOpen={isImporting} /> + + ); + +}; diff --git a/ui/ui-app/src/app/pages/explore/ExploreType.ts b/ui/ui-app/src/app/pages/explore/ExploreType.ts new file mode 100644 index 0000000000..92b7dd290d --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/ExploreType.ts @@ -0,0 +1,4 @@ + +export enum ExploreType { + GROUP = "GROUP", ARTIFACT = "ARTIFACT", VERSION = "VERSION" +} diff --git a/ui/ui-app/src/app/pages/explore/components/artifactList/ArtifactGroup.tsx b/ui/ui-app/src/app/pages/explore/components/artifactList/ArtifactGroup.tsx new file mode 100644 index 0000000000..93c7139e32 --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/components/artifactList/ArtifactGroup.tsx @@ -0,0 +1,42 @@ +import React, { FunctionComponent } from "react"; +import { AppNavigation, useAppNavigation } from "@services/useAppNavigation.ts"; +import { Link } from "react-router-dom"; + +let testIdCounter: number = 1; + +/** + * Properties + */ +export type ArtifactGroupProps = { + groupId: string|null; +}; + + +/** + * Models an artifact group in a list of artifacts or groups. + */ +export const ArtifactGroup: FunctionComponent = (props: ArtifactGroupProps) => { + const appNav: AppNavigation = useAppNavigation(); + + const groupLink = (): string => { + const groupId: string = props.groupId == null ? "default" : props.groupId; + const link: string = `/explore/${ encodeURIComponent(groupId)}`; + return appNav.createLink(link); + }; + + const counter = testIdCounter++; + const testId = (prefix: string): string => { + return `${prefix}-${counter}`; + }; + + const style = (): string => { + return !props.groupId ? "nogroup" : "group"; + }; + + return ( + + {props.groupId} + + ); + +}; diff --git a/ui/ui-app/src/app/pages/artifacts/components/artifactList/ArtifactList.css b/ui/ui-app/src/app/pages/explore/components/artifactList/ArtifactList.css similarity index 98% rename from ui/ui-app/src/app/pages/artifacts/components/artifactList/ArtifactList.css rename to ui/ui-app/src/app/pages/explore/components/artifactList/ArtifactList.css index af10859a01..019ea44cb8 100644 --- a/ui/ui-app/src/app/pages/artifacts/components/artifactList/ArtifactList.css +++ b/ui/ui-app/src/app/pages/explore/components/artifactList/ArtifactList.css @@ -40,21 +40,21 @@ display: none; } -.artifact-list .artifact-list-item .content-cell .artifact-title .name { +.artifact-list .artifact-list-item .content-cell .artifact-title .id { color: #2b9af3; font-weight: bold; font-size: 18px; margin-right: 10px; } -.artifact-list .artifact-list-item .content-cell .artifact-title .id::before { +.artifact-list .artifact-list-item .content-cell .artifact-title .name::before { content: '('; } -.artifact-list .artifact-list-item .content-cell .artifact-title .id { +.artifact-list .artifact-list-item .content-cell .artifact-title .name { color: #2b9af3; font-weight: bold; font-size: 16px; } -.artifact-list .artifact-list-item .content-cell .artifact-title .id::after { +.artifact-list .artifact-list-item .content-cell .artifact-title .name::after { content: ')'; } diff --git a/ui/ui-app/src/app/pages/artifacts/components/artifactList/ArtifactList.tsx b/ui/ui-app/src/app/pages/explore/components/artifactList/ArtifactList.tsx similarity index 97% rename from ui/ui-app/src/app/pages/artifacts/components/artifactList/ArtifactList.tsx rename to ui/ui-app/src/app/pages/explore/components/artifactList/ArtifactList.tsx index aae42c941c..8da50ef145 100644 --- a/ui/ui-app/src/app/pages/artifacts/components/artifactList/ArtifactList.tsx +++ b/ui/ui-app/src/app/pages/explore/components/artifactList/ArtifactList.tsx @@ -10,7 +10,6 @@ import { SearchedArtifact } from "@models/searchedArtifact.model.ts"; */ export type ArtifactListProps = { artifacts: SearchedArtifact[]; - onGroupClick: (groupId: string) => void; }; @@ -53,7 +52,7 @@ export const ArtifactList: FunctionComponent = (props: Artifa ,
- + { statuses(artifact).map( status => diff --git a/ui/ui-app/src/app/pages/artifacts/components/artifactList/ArtifactName.tsx b/ui/ui-app/src/app/pages/explore/components/artifactList/ArtifactName.tsx similarity index 81% rename from ui/ui-app/src/app/pages/artifacts/components/artifactList/ArtifactName.tsx rename to ui/ui-app/src/app/pages/explore/components/artifactList/ArtifactName.tsx index b843d94399..c7d2513d40 100644 --- a/ui/ui-app/src/app/pages/artifacts/components/artifactList/ArtifactName.tsx +++ b/ui/ui-app/src/app/pages/explore/components/artifactList/ArtifactName.tsx @@ -1,5 +1,4 @@ import React, { FunctionComponent } from "react"; -import "./ArtifactList.css"; import { Link } from "react-router-dom"; import { AppNavigation, useAppNavigation } from "@services/useAppNavigation.ts"; @@ -17,7 +16,7 @@ export const ArtifactName: FunctionComponent = (props: Artifa const artifactLink = (): string => { const groupId: string = props.groupId == null ? "default" : props.groupId; - const link: string = `/artifacts/${ encodeURIComponent(groupId)}/${ encodeURIComponent(props.id) }`; + const link: string = `/explore/${ encodeURIComponent(groupId)}/${ encodeURIComponent(props.id) }`; return appNav.createLink(link); }; @@ -28,12 +27,12 @@ export const ArtifactName: FunctionComponent = (props: Artifa return props.name ? ( - {props.name} {props.id} + {props.name} ) : ( - {props.id} + {props.id} ); diff --git a/ui/ui-app/src/app/pages/artifacts/components/artifactList/index.ts b/ui/ui-app/src/app/pages/explore/components/artifactList/index.ts similarity index 100% rename from ui/ui-app/src/app/pages/artifacts/components/artifactList/index.ts rename to ui/ui-app/src/app/pages/explore/components/artifactList/index.ts diff --git a/ui/ui-app/src/app/pages/artifacts/components/empty/ArtifactsPageEmptyState.css b/ui/ui-app/src/app/pages/explore/components/empty/ExplorePageEmptyState.css similarity index 100% rename from ui/ui-app/src/app/pages/artifacts/components/empty/ArtifactsPageEmptyState.css rename to ui/ui-app/src/app/pages/explore/components/empty/ExplorePageEmptyState.css diff --git a/ui/ui-app/src/app/pages/explore/components/empty/ExplorePageEmptyState.tsx b/ui/ui-app/src/app/pages/explore/components/empty/ExplorePageEmptyState.tsx new file mode 100644 index 0000000000..116021f2a9 --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/components/empty/ExplorePageEmptyState.tsx @@ -0,0 +1,91 @@ +import { FunctionComponent } from "react"; +import "./ExplorePageEmptyState.css"; +import { + Button, + EmptyState, + EmptyStateActions, + EmptyStateBody, + EmptyStateFooter, + EmptyStateIcon, + EmptyStateVariant, + Title +} from "@patternfly/react-core"; +import { PlusCircleIcon } from "@patternfly/react-icons"; +import { IfAuth, IfFeature } from "@app/components"; +import { If } from "@apicurio/common-ui-components"; +import { ExploreType } from "@app/pages/explore/ExploreType.ts"; + +/** + * Properties + */ +export type ExplorePageEmptyStateProps = { + exploreType: ExploreType; + isFiltered: boolean; + onCreateArtifact: () => void; + onCreateGroup: () => void; + onImport: () => void; +}; + + +/** + * Models the empty state for the Explore page (when there are no results). + */ +export const ExplorePageEmptyState: FunctionComponent = (props: ExplorePageEmptyStateProps) => { + let entitySingular: string; + let entityPlural: string; + switch (props.exploreType) { + case ExploreType.ARTIFACT: + entitySingular = "artifact"; + entityPlural = "artifacts"; + break; + case ExploreType.GROUP: + entitySingular = "group"; + entityPlural = "groups"; + break; + case ExploreType.VERSION: + entitySingular = "version"; + entityPlural = "versions"; + break; + } + return ( + + + No { entityPlural } found + props.isFiltered}> + + No {entityPlural} match your filter settings. Change your filter or perhaps Create a new {entitySingular}. + + + !props.isFiltered}> + + There are currently no {entityPlural} in the registry. Create one or more {entityPlural} to view them here. + + + + + + + + + + + + + + + + + + + + + + + + + ); + +}; diff --git a/ui/ui-app/src/app/pages/explore/components/empty/index.ts b/ui/ui-app/src/app/pages/explore/components/empty/index.ts new file mode 100644 index 0000000000..6abe96d3e3 --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/components/empty/index.ts @@ -0,0 +1 @@ +export * from "./ExplorePageEmptyState.tsx"; diff --git a/ui/ui-app/src/app/pages/explore/components/groupList/GroupList.css b/ui/ui-app/src/app/pages/explore/components/groupList/GroupList.css new file mode 100644 index 0000000000..3aa0fa98f5 --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/components/groupList/GroupList.css @@ -0,0 +1,48 @@ +.group-list { +} + +.group-list .group-list-item { + background-color: white; + margin-bottom: 2px; +} + +.group-list .group-list-item .type-icon-cell { + flex: initial; +} + +.group-list .group-list-item .type-icon-cell > .type-icon { + margin-top: 5px; +} + +.group-list .group-list-item .content-cell { +} + +.group-list .group-list-item .content-cell .group-title { +} + +.group-list .group-list-item .content-cell .group-title a, .group-list .group-list-item .content-cell .group-title a:hover { + text-decoration: none; +} + +.group-list .group-list-item .content-cell .group-title .group { + color: #2b9af3; + font-weight: bold; +} + +.group-list .group-list-item .content-cell .group-title .name { + color: #2b9af3; + font-weight: bold; + font-size: 18px; + margin-right: 10px; +} + +.group-list .group-list-item .content-cell .group-description { + font-size: 13px; +} + +.group-list .group-list-item .content-cell .group-tags { +} + +.group-list .group-list-item .content-cell .group-tags > span { + margin-right: 5px; +} diff --git a/ui/ui-app/src/app/pages/explore/components/groupList/GroupList.tsx b/ui/ui-app/src/app/pages/explore/components/groupList/GroupList.tsx new file mode 100644 index 0000000000..0673cfe67f --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/components/groupList/GroupList.tsx @@ -0,0 +1,65 @@ +import { FunctionComponent } from "react"; +import "./GroupList.css"; +import { Badge, DataList, DataListCell, DataListItemCells, DataListItemRow, Icon } from "@patternfly/react-core"; +import { SearchedGroup } from "@models/searchedGroup.model.ts"; +import { OutlinedFolderIcon } from "@patternfly/react-icons"; +import { ArtifactGroup } from "@app/pages"; + +/** + * Properties + */ +export type GroupListProps = { + groups: SearchedGroup[]; +}; + + +/** + * Models the list of groups. + */ +export const GroupList: FunctionComponent = (props: GroupListProps) => { + + const labels = (group: SearchedGroup): string[] => { + return group.labels ? group.labels : []; + }; + + const description = (group: SearchedGroup): string => { + if (group.description) { + return group.description; + } + return "A group with no description."; + }; + + return ( + + { + props.groups?.map( (group, /* idx */) => + + + + + + , + +
+ +
+
{description(group)}
+
+ { + labels(group).map( label => + {label} + ) + } +
+
+ ]} + /> +
+ ) + } +
+ ); + +}; diff --git a/ui/ui-app/src/app/pages/explore/components/groupList/index.ts b/ui/ui-app/src/app/pages/explore/components/groupList/index.ts new file mode 100644 index 0000000000..1c6ba4a702 --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/components/groupList/index.ts @@ -0,0 +1 @@ +export * from "./GroupList.tsx"; diff --git a/ui/ui-app/src/app/pages/artifacts/components/index.ts b/ui/ui-app/src/app/pages/explore/components/index.ts similarity index 60% rename from ui/ui-app/src/app/pages/artifacts/components/index.ts rename to ui/ui-app/src/app/pages/explore/components/index.ts index 931c4d83ae..da452a2a42 100644 --- a/ui/ui-app/src/app/pages/artifacts/components/index.ts +++ b/ui/ui-app/src/app/pages/explore/components/index.ts @@ -1,4 +1,5 @@ export * from "./artifactList"; +export * from "./groupList"; export * from "./empty"; +export * from "./modals"; export * from "./toolbar"; -export * from "./uploadForm"; diff --git a/ui/ui-app/src/app/pages/explore/components/modals/ImportModal.tsx b/ui/ui-app/src/app/pages/explore/components/modals/ImportModal.tsx new file mode 100644 index 0000000000..043e970390 --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/components/modals/ImportModal.tsx @@ -0,0 +1,94 @@ +import { FunctionComponent, useState } from "react"; +import { + Button, + FileUpload, + Form, + FormGroup, + FormHelperText, + HelperText, + HelperTextItem, + Modal +} from "@patternfly/react-core"; + + +/** + * Properties + */ +export type ImportModalProps = { + isOpen: boolean; + onClose: () => void; + onImport: (file: File | undefined) => void; +}; + +/** + * Models the Import from .ZIP modal dialog. + */ +export const ImportModal: FunctionComponent = (props: ImportModalProps) => { + const [filename, setFilename] = useState(""); + const [file, setFile] = useState(); + const [isFormValid, setFormValid] = useState(false); + + const onFileChange = (_event: any, file: File): void => { + const filename: string = file.name; + const isValid: boolean = filename.toLowerCase().endsWith(".zip"); + setFilename(filename); + setFile(file); + setFormValid(isValid); + }; + + const fireCloseEvent = (): void => { + props.onClose(); + setFilename(""); + setFile(undefined); + setFormValid(false); + }; + + const fireImportEvent = (): void => { + props.onImport(file); + setFilename(""); + setFile(undefined); + setFormValid(false); + }; + + return ( + Import, + + ]} + > +
+ +

+ Select a .zip file previously exported from a Registry instance. +

+
+ + + + + File format must be .zip + + + +
+
+ ); + +}; diff --git a/ui/ui-app/src/app/pages/explore/components/modals/index.ts b/ui/ui-app/src/app/pages/explore/components/modals/index.ts new file mode 100644 index 0000000000..e8f34e5c5f --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/components/modals/index.ts @@ -0,0 +1 @@ +export * from "./ImportModal"; diff --git a/ui/ui-app/src/app/pages/artifacts/components/toolbar/ArtifactsPageToolbar.css b/ui/ui-app/src/app/pages/explore/components/toolbar/ExplorePageToolbar.css similarity index 100% rename from ui/ui-app/src/app/pages/artifacts/components/toolbar/ArtifactsPageToolbar.css rename to ui/ui-app/src/app/pages/explore/components/toolbar/ExplorePageToolbar.css diff --git a/ui/ui-app/src/app/pages/explore/components/toolbar/ExplorePageToolbar.tsx b/ui/ui-app/src/app/pages/explore/components/toolbar/ExplorePageToolbar.tsx new file mode 100644 index 0000000000..a6b57ac443 --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/components/toolbar/ExplorePageToolbar.tsx @@ -0,0 +1,247 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import "./ExplorePageToolbar.css"; +import { + Button, + ButtonVariant, + capitalize, + Form, + InputGroup, + Pagination, + TextInput, + Toolbar, + ToolbarContent, + ToolbarItem +} from "@patternfly/react-core"; +import { SearchIcon, SortAlphaDownAltIcon, SortAlphaDownIcon } from "@patternfly/react-icons"; +import { IfAuth, IfFeature } from "@app/components"; +import { OnPerPageSelect, OnSetPage } from "@patternfly/react-core/dist/js/components/Pagination/Pagination"; +import { If, ObjectDropdown, ObjectSelect } from "@apicurio/common-ui-components"; +import { useLoggerService } from "@services/useLoggerService.ts"; +import { ExploreType } from "@app/pages/explore/ExploreType.ts"; +import { ArtifactSearchResults } from "@models/artifactSearchResults.model.ts"; +import { GroupSearchResults } from "@models/groupSearchResults.model.ts"; +import { plural } from "pluralize"; +import { Paging } from "@models/paging.model.ts"; +import { FilterBy } from "@services/useSearchService.ts"; + +export type ExplorePageToolbarFilterCriteria = { + filterBy: FilterBy; + filterValue: string; + ascending: boolean; +}; + +export type ExplorePageToolbarProps = { + exploreType: ExploreType; + results: ArtifactSearchResults | GroupSearchResults; + onExploreTypeChange: (exploreType: ExploreType) => void; + onCriteriaChange: (criteria: ExplorePageToolbarFilterCriteria) => void; + criteria: ExplorePageToolbarFilterCriteria; + paging: Paging; + onPerPageSelect: OnPerPageSelect; + onSetPage: OnSetPage; + onCreateArtifact: () => void; + onCreateGroup: () => void; + onImport: () => void; + onExport: () => void; +}; + +type FilterType = { + value: FilterBy; + label: string; + testId: string; +}; +const ARTIFACT_FILTER_TYPES: FilterType[] = [ + { value: FilterBy.name, label: "Name", testId: "artifact-filter-typename" }, + { value: FilterBy.groupId, label: "Group", testId: "artifact-filter-typegroup" }, + { value: FilterBy.description, label: "Description", testId: "artifact-filter-typedescription" }, + { value: FilterBy.labels, label: "Labels", testId: "artifact-filter-typelabels" }, + { value: FilterBy.globalId, label: "Global Id", testId: "artifact-filter-typeglobal-id" }, + { value: FilterBy.contentId, label: "Content Id", testId: "artifact-filter-typecontent-id" }, +]; +const GROUP_FILTER_TYPES: FilterType[] = [ + { value: FilterBy.groupId, label: "Group", testId: "group-filter-typegroup" }, + { value: FilterBy.description, label: "Description", testId: "group-filter-typedescription" }, + { value: FilterBy.labels, label: "Labels", testId: "group-filter-typelabels" }, +]; + + +type ActionType = { + label: string; + callback: () => void; +}; + +/** + * Models the toolbar for the Explore page. + */ +export const ExplorePageToolbar: FunctionComponent = (props: ExplorePageToolbarProps) => { + const [artifactFilterType, setArtifactFilterType] = useState(ARTIFACT_FILTER_TYPES[0]); + const [groupFilterType, setGroupFilterType] = useState(GROUP_FILTER_TYPES[0]); + const [filterValue, setFilterValue] = useState(""); + const [filterAscending, setFilterAscending] = useState(true); + const [kebabActions, setKebabActions] = useState([]); + + const logger = useLoggerService(); + + const totalArtifactsCount = (): number => { + return props.results.count; + }; + + const onFilterSubmit = (event: any|undefined): void => { + const filterTypeValue: FilterBy = (props.exploreType === ExploreType.ARTIFACT) ? artifactFilterType.value : groupFilterType.value; + fireChangeEvent(filterAscending, filterTypeValue, filterValue); + if (event) { + event.preventDefault(); + } + }; + + const onArtifactFilterTypeChange = (newType: FilterType): void => { + setArtifactFilterType(newType); + fireChangeEvent(filterAscending, newType.value, filterValue); + }; + + const onGroupFilterTypeChange = (newType: FilterType): void => { + setGroupFilterType(newType); + fireChangeEvent(filterAscending, newType.value, filterValue); + }; + + const onToggleAscending = (): void => { + logger.debug("[ExplorePageToolbar] Toggle the ascending flag."); + const filterTypeValue: FilterBy = (props.exploreType === ExploreType.ARTIFACT) ? artifactFilterType.value : groupFilterType.value; + const newAscending: boolean = !filterAscending; + setFilterAscending(newAscending); + fireChangeEvent(newAscending, filterTypeValue, filterValue); + }; + + const fireChangeEvent = (ascending: boolean, filterBy: FilterBy, filterValue: string): void => { + const criteria: ExplorePageToolbarFilterCriteria = { + ascending, + filterBy, + filterValue + }; + props.onCriteriaChange(criteria); + }; + + const onExploreTypeChange = (newExploreType: ExploreType): void => { + setFilterAscending(true); + setFilterValue(""); + if (newExploreType === ExploreType.ARTIFACT) { + setArtifactFilterType(ARTIFACT_FILTER_TYPES[0]); + } else if (newExploreType === ExploreType.GROUP) { + setGroupFilterType(GROUP_FILTER_TYPES[0]); + } + props.onExploreTypeChange(newExploreType); + }; + + useEffect(() => { + const adminActions: ActionType[] = [ + { label: "Import from .ZIP", callback: props.onImport }, + { label: "Export all (as .ZIP)", callback: props.onExport } + ]; + setKebabActions(adminActions); + }, [props.onExport, props.onImport]); + + return ( + + + + Search for + + + `explore-type-${plural(item.toString().toLowerCase())}`} + itemToString={(item) => capitalize(plural(item.toString().toLowerCase()))} /> + + + filter by + + +
+ + + item.testId} + itemToString={(item) => item.label} /> + + + item.testId} + itemToString={(item) => item.label} /> + + setFilterValue(value)} + data-testid="artifact-filter-value" + aria-label="search input example"/> + + +
+
+ + + + + + + + + + + + + + + + + + item.callback()} + itemToString={(item) => item.label} + isKebab={true} /> + + + + + +
+
+ ); + +}; diff --git a/ui/ui-app/src/app/pages/explore/components/toolbar/index.ts b/ui/ui-app/src/app/pages/explore/components/toolbar/index.ts new file mode 100644 index 0000000000..82a080b8b4 --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/components/toolbar/index.ts @@ -0,0 +1 @@ +export * from "./ExplorePageToolbar.tsx"; diff --git a/ui/ui-app/src/app/pages/explore/index.ts b/ui/ui-app/src/app/pages/explore/index.ts new file mode 100644 index 0000000000..47b368cdef --- /dev/null +++ b/ui/ui-app/src/app/pages/explore/index.ts @@ -0,0 +1,2 @@ +export * from "./ExplorePage.tsx"; +export * from "./components"; diff --git a/ui/ui-app/src/app/pages/artifactVersion/ArtifactVersionPage.css b/ui/ui-app/src/app/pages/group/GroupPage.css similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/ArtifactVersionPage.css rename to ui/ui-app/src/app/pages/group/GroupPage.css diff --git a/ui/ui-app/src/app/pages/group/GroupPage.tsx b/ui/ui-app/src/app/pages/group/GroupPage.tsx new file mode 100644 index 0000000000..0eb9503e2d --- /dev/null +++ b/ui/ui-app/src/app/pages/group/GroupPage.tsx @@ -0,0 +1,275 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import "./GroupPage.css"; +import { Breadcrumb, BreadcrumbItem, PageSection, PageSectionVariants, Tab, Tabs } from "@patternfly/react-core"; +import { Link, useParams } from "react-router-dom"; +import { + GroupInfoTabContent, + GroupPageHeader, + PageDataLoader, + PageError, + PageErrorHandler, + toPageError +} from "@app/pages"; +import { + ChangeOwnerModal, + ConfirmDeleteModal, CreateArtifactModal, + EditMetaDataModal, + IfFeature, + InvalidContentModal, + MetaData +} from "@app/components"; +import { PleaseWaitModal } from "@apicurio/common-ui-components"; +import { AppNavigation, useAppNavigation } from "@services/useAppNavigation.ts"; +import { LoggerService, useLoggerService } from "@services/useLoggerService.ts"; +import { GroupsService, useGroupsService } from "@services/useGroupsService.ts"; +import { GroupMetaData } from "@models/groupMetaData.model.ts"; +import { ArtifactsTabContent } from "@app/pages/group/components/tabs/ArtifactsTabContent.tsx"; +import { ApiError } from "@models/apiError.model.ts"; +import { SearchedArtifact } from "@models/searchedArtifact.model.ts"; +import { CreateArtifact } from "@models/createArtifact.model.ts"; + + +export type GroupPageProps = { + // No properties +} + +/** + * The group page. + */ +export const GroupPage: FunctionComponent = () => { + const [pageError, setPageError] = useState(); + const [loaders, setLoaders] = useState | Promise[] | undefined>(); + const [activeTabKey, setActiveTabKey] = useState("overview"); + const [group, setGroup] = useState(); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isDeleteArtifactModalOpen, setIsDeleteArtifactModalOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isChangeOwnerModalOpen, setIsChangeOwnerModalOpen] = useState(false); + const [isPleaseWaitModalOpen, setIsPleaseWaitModalOpen] = useState(false); + const [pleaseWaitMessage, setPleaseWaitMessage] = useState(""); + const [isCreateArtifactModalOpen, setCreateArtifactModalOpen] = useState(false); + const [invalidContentError, setInvalidContentError] = useState(); + const [isInvalidContentModalOpen, setInvalidContentModalOpen] = useState(false); + const [artifactToDelete, setArtifactToDelete] = useState(); + const [artifactDeleteSuccessCallback, setArtifactDeleteSuccessCallback] = useState<() => void>(); + + const appNavigation: AppNavigation = useAppNavigation(); + const logger: LoggerService = useLoggerService(); + const groups: GroupsService = useGroupsService(); + const { groupId }= useParams(); + + const createLoaders = (): Promise[] => { + logger.info("Loading data for group: ", groupId); + return [ + groups.getGroupMetaData(groupId as string) + .then(setGroup) + .catch(error => { + setPageError(toPageError(error, "Error loading page data.")); + }), + ]; + }; + + const handleTabClick = (_event: any, tabIndex: any): void => { + setActiveTabKey(tabIndex); + }; + + const onDeleteGroup = (): void => { + setIsDeleteModalOpen(true); + }; + + const onDeleteModalClose = (): void => { + setIsDeleteModalOpen(false); + }; + + const onCreateArtifact = (): void => { + setCreateArtifactModalOpen(true); + }; + + const onCreateArtifactModalClose = (): void => { + setCreateArtifactModalOpen(false); + }; + + const doDeleteGroup = (): void => { + onDeleteModalClose(); + pleaseWait(true, "Deleting group, please wait."); + groups.deleteGroup(groupId as string).then( () => { + pleaseWait(false); + appNavigation.navigateTo("/explore"); + }); + }; + + const doDeleteArtifact = (): void => { + setIsDeleteArtifactModalOpen(false); + pleaseWait(true, "Deleting artifact, please wait."); + groups.deleteArtifact(groupId as string, artifactToDelete?.artifactId as string).then( () => { + pleaseWait(false); + if (artifactDeleteSuccessCallback) { + artifactDeleteSuccessCallback(); + } + }); + }; + + const doCreateArtifact = (_groupId: string|null, data: CreateArtifact): void => { + // Note: the create artifact modal passes the groupId, but we don't care about that because + // this is the group page, so we know we want to create the artifact within this group! + onCreateArtifactModalClose(); + pleaseWait(true, "Creating artifact, please wait."); + groups.createArtifact(group?.groupId as string, data).then(response => { + const groupId: string = response.artifact.groupId || "default"; + const artifactLocation: string = `/explore/${ encodeURIComponent(groupId) }/${ encodeURIComponent(response.artifact.artifactId) }`; + logger.info("[ExplorePage] Artifact successfully created. Redirecting to details page: ", artifactLocation); + appNavigation.navigateTo(artifactLocation); + }).catch( error => { + pleaseWait(false); + if (error && (error.error_code === 400 || error.error_code === 409)) { + handleInvalidContentError(error); + } else { + setPageError(toPageError(error, "Error creating artifact.")); + } + }); + }; + + const closeInvalidContentModal = (): void => { + setInvalidContentModalOpen(false); + }; + + const handleInvalidContentError = (error: any): void => { + logger.info("[ExplorePage] Invalid content error:", error); + setInvalidContentError(error); + setInvalidContentModalOpen(true); + }; + + const onEditModalClose = (): void => { + setIsEditModalOpen(false); + }; + + const onChangeOwnerModalClose = (): void => { + setIsChangeOwnerModalOpen(false); + }; + + const doEditMetaData = (metaData: MetaData): void => { + groups.updateGroupMetaData(groupId as string, metaData).then( () => { + setGroup({ + ...(group as GroupMetaData), + ...metaData + }); + }).catch( error => { + setPageError(toPageError(error, "Error editing group metadata.")); + }); + onEditModalClose(); + }; + + const doChangeOwner = (newOwner: string): void => { + groups.updateGroupOwner(groupId as string, newOwner).then( () => { + setGroup({ + ...(group as GroupMetaData), + owner: newOwner + }); + }).catch( error => { + setPageError(toPageError(error, "Error changing group ownership.")); + }); + onChangeOwnerModalClose(); + }; + + const onViewArtifact = (artifact: SearchedArtifact): void => { + const groupId: string = encodeURIComponent(group?.groupId || "default"); + const artifactId: string = encodeURIComponent(artifact.artifactId); + appNavigation.navigateTo(`/explore/${groupId}/${artifactId}`); + }; + + const onDeleteArtifact = (artifact: SearchedArtifact, deleteSuccessCallback?: () => void): void => { + setArtifactToDelete(artifact); + setIsDeleteArtifactModalOpen(true); + setArtifactDeleteSuccessCallback(() => deleteSuccessCallback); + }; + + const pleaseWait = (isOpen: boolean, message: string = ""): void => { + setIsPleaseWaitModalOpen(isOpen); + setPleaseWaitMessage(message); + }; + + useEffect(() => { + setLoaders(createLoaders()); + }, [groupId]); + + const tabs: any[] = [ + + setIsEditModalOpen(true)} onChangeOwner={() => {}} /> + , + + + , + ]; + + const breadcrumbs = ( + + Explore + { groupId as string } + + ); + + return ( + + + + + + + + + + + + + + + {setIsDeleteArtifactModalOpen(false);}} /> + + + + + + ); + +}; diff --git a/ui/ui-app/src/app/pages/group/components/index.ts b/ui/ui-app/src/app/pages/group/components/index.ts new file mode 100644 index 0000000000..cc8a7498bd --- /dev/null +++ b/ui/ui-app/src/app/pages/group/components/index.ts @@ -0,0 +1,2 @@ +export * from "./pageheader"; +export * from "./tabs"; diff --git a/ui/ui-app/src/app/pages/group/components/pageheader/GroupPageHeader.css b/ui/ui-app/src/app/pages/group/components/pageheader/GroupPageHeader.css new file mode 100644 index 0000000000..b87bd8c237 --- /dev/null +++ b/ui/ui-app/src/app/pages/group/components/pageheader/GroupPageHeader.css @@ -0,0 +1,7 @@ +#upload-version-button { +} + +#delete-artifact-button { + margin-left: 10px; + margin-right: 10px; +} diff --git a/ui/ui-app/src/app/pages/group/components/pageheader/GroupPageHeader.tsx b/ui/ui-app/src/app/pages/group/components/pageheader/GroupPageHeader.tsx new file mode 100644 index 0000000000..3bdfffe405 --- /dev/null +++ b/ui/ui-app/src/app/pages/group/components/pageheader/GroupPageHeader.tsx @@ -0,0 +1,37 @@ +import { FunctionComponent } from "react"; +import "./GroupPageHeader.css"; +import { Button, Flex, FlexItem, Text, TextContent, TextVariants } from "@patternfly/react-core"; +import { IfAuth, IfFeature } from "@app/components"; + + +/** + * Properties + */ +export type GroupPageHeaderProps = { + title: string; + groupId: string; + onDeleteGroup: () => void; +}; + +/** + * Models the page header for the Group page. + */ +export const GroupPageHeader: FunctionComponent = (props: GroupPageHeaderProps) => { + return ( + + + + { props.title } + + + + + + + + + + + ); +}; diff --git a/ui/ui-app/src/app/pages/group/components/pageheader/index.ts b/ui/ui-app/src/app/pages/group/components/pageheader/index.ts new file mode 100644 index 0000000000..ad4e107b34 --- /dev/null +++ b/ui/ui-app/src/app/pages/group/components/pageheader/index.ts @@ -0,0 +1 @@ +export * from "./GroupPageHeader.tsx"; diff --git a/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTabContent.css b/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTabContent.css new file mode 100644 index 0000000000..02af7ae19b --- /dev/null +++ b/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTabContent.css @@ -0,0 +1,10 @@ +.artifacts-tab-content { + padding: 22px; + display: flex; + flex-direction: column; + background-color: rgb(240,240,240); +} + +.artifacts-tab-content > div { + background-color: white; +} diff --git a/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTabContent.tsx b/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTabContent.tsx new file mode 100644 index 0000000000..f3011aacc6 --- /dev/null +++ b/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTabContent.tsx @@ -0,0 +1,134 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import "./ArtifactsTabContent.css"; +import "@app/styles/empty.css"; +import { ListWithToolbar } from "@apicurio/common-ui-components"; +import { GroupMetaData } from "@models/groupMetaData.model.ts"; +import { ArtifactsTabToolbar } from "@app/pages/group/components/tabs/ArtifactsTabToolbar.tsx"; +import { Paging } from "@models/paging.model.ts"; +import { ArtifactSearchResults } from "@models/artifactSearchResults.model.ts"; +import { LoggerService, useLoggerService } from "@services/useLoggerService.ts"; +import { + Button, + EmptyState, + EmptyStateActions, + EmptyStateBody, + EmptyStateFooter, + EmptyStateIcon, + EmptyStateVariant, + Title +} from "@patternfly/react-core"; +import { PlusCircleIcon } from "@patternfly/react-icons"; +import { IfAuth, IfFeature } from "@app/components"; +import { ArtifactsTable } from "@app/pages/group/components/tabs/ArtifactsTable.tsx"; +import { GroupsService, useGroupsService } from "@services/useGroupsService.ts"; +import { ArtifactSortBy } from "@models/artifactSortBy.model.ts"; +import { SortOrder } from "@models/sortOrder.model.ts"; +import { SearchedArtifact } from "@models/searchedArtifact.model.ts"; + +/** + * Properties + */ +export type ArtifactsTabContentProps = { + group: GroupMetaData; + onCreateArtifact: () => void; + onDeleteArtifact: (artifact: SearchedArtifact, successCallback?: () => void) => void; + onViewArtifact: (artifact: SearchedArtifact) => void; +}; + +/** + * Models the content of the Artifact Info tab. + */ +export const ArtifactsTabContent: FunctionComponent = (props: ArtifactsTabContentProps) => { + const [isLoading, setLoading] = useState(true); + const [isError, setError] = useState(false); + const [paging, setPaging] = useState({ + page: 1, + pageSize: 20 + }); + const [sortBy, setSortBy] = useState(ArtifactSortBy.artifactId); + const [sortOrder, setSortOrder] = useState(SortOrder.asc); + const [results, setResults] = useState({ + count: 0, + artifacts: [] + }); + + const groups: GroupsService = useGroupsService(); + const logger: LoggerService = useLoggerService(); + + const refresh = (): void => { + setLoading(true); + + groups.getGroupArtifacts(props.group.groupId, sortBy, sortOrder, paging).then(sr => { + setResults(sr); + setLoading(false); + }).catch(error => { + logger.error(error); + setLoading(false); + setError(true); + }); + }; + + const onDelete = (artifact: SearchedArtifact): void => { + props.onDeleteArtifact(artifact, () => { + setTimeout(refresh, 100); + }); + }; + + useEffect(() => { + refresh(); + }, [props.group, paging, sortBy, sortOrder]); + + const onSort = (by: ArtifactSortBy, order: SortOrder): void => { + setSortBy(by); + setSortOrder(order); + }; + + const toolbar = ( + + ); + + const emptyState = ( + + + No artifacts found + + There are currently no artifacts in this group. Create some artifacts in the group to view them here. + + + + + + + + + + + + ); + + return ( +
+
+ + + +
+
+ ); + +}; diff --git a/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTabToolbar.css b/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTabToolbar.css new file mode 100644 index 0000000000..9181b970e4 --- /dev/null +++ b/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTabToolbar.css @@ -0,0 +1,17 @@ +.artifacts-toolbar { + padding-left: 8px; + padding-right: 24px; +} + +.artifacts-toolbar > div { + width: 100%; +} + +.artifacts-toolbar .filter-types-toggle span.pf-v5-c-menu-toggle__text { + width: 125px; + text-align: left; +} + +#artifacts-toolbar-1 .tbi-filter-type { + margin-right: 0; +} diff --git a/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTabToolbar.tsx b/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTabToolbar.tsx new file mode 100644 index 0000000000..1c63c608b1 --- /dev/null +++ b/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTabToolbar.tsx @@ -0,0 +1,68 @@ +import { FunctionComponent } from "react"; +import "./ArtifactsTabToolbar.css"; +import { Button, Pagination, Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core"; +import { Paging } from "@models/paging.model.ts"; +import { ArtifactSearchResults } from "@models/artifactSearchResults.model.ts"; +import { IfAuth, IfFeature } from "@app/components"; + + +/** + * Properties + */ +export type ArtifactsToolbarProps = { + results: ArtifactSearchResults; + paging: Paging; + onPageChange: (paging: Paging) => void; + onCreateArtifact: () => void; +}; + + +/** + * Models the toolbar for the Artifacts tab on the Group page. + */ +export const ArtifactsTabToolbar: FunctionComponent = (props: ArtifactsToolbarProps) => { + + const onSetPage = (_event: any, newPage: number, perPage?: number): void => { + const newPaging: Paging = { + page: newPage, + pageSize: perPage ? perPage : props.paging.pageSize + }; + props.onPageChange(newPaging); + }; + + const onPerPageSelect = (_event: any, newPerPage: number): void => { + const newPaging: Paging = { + page: props.paging.page, + pageSize: newPerPage + }; + props.onPageChange(newPaging); + }; + + return ( + + + + + + + + + + + + + + + ); +}; diff --git a/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTable.tsx b/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTable.tsx new file mode 100644 index 0000000000..9fbcdbe544 --- /dev/null +++ b/ui/ui-app/src/app/pages/group/components/tabs/ArtifactsTable.tsx @@ -0,0 +1,162 @@ +import React, { FunctionComponent, useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { SortByDirection, ThProps } from "@patternfly/react-table"; +import { FromNow, ObjectDropdown, ResponsiveTable } from "@apicurio/common-ui-components"; +import { SearchedArtifact } from "@models/searchedArtifact.model.ts"; +import { AppNavigation, useAppNavigation } from "@services/useAppNavigation.ts"; +import { ArtifactDescription, ArtifactTypeIcon } from "@app/components"; +import { shash } from "@utils/string.utils.ts"; +import { ArtifactSortBy } from "@models/artifactSortBy.model.ts"; +import { SortOrder } from "@models/sortOrder.model.ts"; +import { Truncate } from "@patternfly/react-core"; + +export type ArtifactsTableProps = { + artifacts: SearchedArtifact[]; + sortBy: ArtifactSortBy; + sortOrder: SortOrder; + onSort: (by: ArtifactSortBy, order: SortOrder) => void; + onView: (artifact: SearchedArtifact) => void; + onDelete: (artifact: SearchedArtifact) => void; +} +type ArtifactAction = { + label: string; + testId: string; + onClick: () => void; +}; + +type ArtifactActionSeparator = { + isSeparator: true; +}; + +export const ArtifactsTable: FunctionComponent = (props: ArtifactsTableProps) => { + const [sortByIndex, setSortByIndex] = useState(); + + const appNavigation: AppNavigation = useAppNavigation(); + + const columns: any[] = [ + { index: 0, id: "artifactId", label: "Artifact Id", width: 40, sortable: true, sortBy: ArtifactSortBy.artifactId }, + { index: 1, id: "type", label: "Type", width: 15, sortable: true, sortBy: ArtifactSortBy.artifactType }, + { index: 2, id: "createdOn", label: "Created on", width: 15, sortable: true, sortBy: ArtifactSortBy.createdOn }, + { index: 3, id: "modifiedOn", label: "Modified on", width: 15, sortable: true, sortBy: ArtifactSortBy.modifiedOn }, + ]; + + const idAndName = (artifact: SearchedArtifact): string => { + return artifact.artifactId + (artifact.name ? ` (${artifact.name})` : ""); + }; + + const renderColumnData = (column: SearchedArtifact, colIndex: number): React.ReactNode => { + // Name. + if (colIndex === 0) { + return ( +
+ + + + +
+ ); + } + // Type. + if (colIndex === 1) { + return ( + + ); + } + // Created on. + if (colIndex === 2) { + return ( + + ); + } + // Modified on. + if (colIndex === 3) { + return ( + + ); + } + }; + + const actionsFor = (artifact: SearchedArtifact): (ArtifactAction | ArtifactActionSeparator)[] => { + const ahash: number = shash(artifact.artifactId); + return [ + { label: "View artifact", onClick: () => props.onView(artifact), testId: `view-artifact-${ahash}` }, + { isSeparator: true }, + { label: "Delete artifact", onClick: () => props.onDelete(artifact), testId: `delete-artifact-${ahash}` } + ]; + }; + + const sortParams = (column: any): ThProps["sort"] | undefined => { + return column.sortable ? { + sortBy: { + index: sortByIndex, + direction: props.sortOrder + }, + onSort: (_event, index, direction) => { + props.onSort(columns[index].sortBy, direction === SortByDirection.asc ? SortOrder.asc : SortOrder.desc); + }, + columnIndex: column.index + } : undefined; + }; + + useEffect(() => { + if (props.sortBy === ArtifactSortBy.artifactId) { + setSortByIndex(0); + } + if (props.sortBy === ArtifactSortBy.artifactType) { + setSortByIndex(1); + } + if (props.sortBy === ArtifactSortBy.createdOn) { + setSortByIndex(2); + } + if (props.sortBy === ArtifactSortBy.modifiedOn) { + setSortByIndex(3); + } + }, [props.sortBy]); + + return ( +
+ { + console.log(row); + }} + renderHeader={({ column, Th }) => ( + {column.label} + )} + renderCell={({ row, colIndex, Td }) => ( + + )} + renderActions={({ row }) => ( + item.label} + itemToTestId={item => item.testId} + itemIsDivider={item => item.isSeparator} + onSelect={item => item.onClick()} + testId={`api-actions-${shash(row.artifactId)}`} + popperProps={{ + position: "right" + }} + /> + )} + /> +
+ ); +}; diff --git a/ui/ui-app/src/app/pages/group/components/tabs/GroupInfoTabContent.css b/ui/ui-app/src/app/pages/group/components/tabs/GroupInfoTabContent.css new file mode 100644 index 0000000000..d14bb9013d --- /dev/null +++ b/ui/ui-app/src/app/pages/group/components/tabs/GroupInfoTabContent.css @@ -0,0 +1,53 @@ +.group-tab-content { + padding: 22px; + display: flex; + flex-direction: row; + background-color: rgb(240,240,240); +} + +.group-tab-content .group-basics { + flex-grow: 1; + flex-basis: 40%; + margin-right: 15px; +} + +.group-tab-content .group-basics .title-and-type { +} + +.group-tab-content .group-basics .title-and-type .type { +} + +.group-tab-content .group-basics .title-and-type .title { + font-size: 17px; + font-weight: bold; + line-height: 24px; + display: inline-block; +} + +.group-tab-content .group-basics .metaData { + margin-bottom: 20px; +} + +.group-tab-content .group-basics .actions { +} + + +.group-tab-content .group-rules { + flex-grow: 1; + flex-basis: 60%; +} + +.group-tab-content .group-rules .rules-label { + font-size: 17px; + font-weight: bold; + line-height: 29px; + padding-bottom: 7px; +} + +.label-truncate, .label-truncate, .label-truncate > span, .label-truncate > span { + min-width: unset; +} + +#generate-client-action { + margin-left: 10px; +} diff --git a/ui/ui-app/src/app/pages/group/components/tabs/GroupInfoTabContent.tsx b/ui/ui-app/src/app/pages/group/components/tabs/GroupInfoTabContent.tsx new file mode 100644 index 0000000000..d29b240414 --- /dev/null +++ b/ui/ui-app/src/app/pages/group/components/tabs/GroupInfoTabContent.tsx @@ -0,0 +1,151 @@ +import { FunctionComponent } from "react"; +import "./GroupInfoTabContent.css"; +import "@app/styles/empty.css"; +import { IfAuth, IfFeature } from "@app/components"; +import { + Button, + Card, + CardBody, + CardTitle, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Divider, + Flex, + FlexItem, + Icon, + Label, + Truncate +} from "@patternfly/react-core"; +import { IndustryIcon, OutlinedFolderIcon, PencilAltIcon } from "@patternfly/react-icons"; +import { FromNow, If } from "@apicurio/common-ui-components"; +import { GroupMetaData } from "@models/groupMetaData.model.ts"; +import { isStringEmptyOrUndefined } from "@utils/string.utils.ts"; + +/** + * Properties + */ +export type GroupInfoTabContentProps = { + group: GroupMetaData; + onEditMetaData: () => void; + onChangeOwner: () => void; +}; + +/** + * Models the content of the Artifact Info tab. + */ +export const GroupInfoTabContent: FunctionComponent = (props: GroupInfoTabContentProps) => { + + const description = (): string => { + return props.group.description || "No description"; + }; + + return ( +
+
+ + +
+ + + Group metadata + + + + + + + + +
+
+ + + + + Description + + { description() } + + + + Created + + + + + + + Owner + + {props.group.owner} + + + + + + + + + + + + Modified + + + + + + Labels + {!props.group.labels || !Object.keys(props.group.labels).length ? + No labels : + {Object.entries(props.group.labels).map(([key, value]) => + + )} + } + + + +
+
+
+ + +
Group-specific rules
+
+ + +

+ Manage the content rules for this group. Each group-specific rule can be + individually enabled, configured, and disabled. Group-specific rules override + the equivalent global rules. +

+

+ Under construction +

+ {/**/} +
+
+
+
+ ); + +}; diff --git a/ui/ui-app/src/app/pages/group/components/tabs/index.ts b/ui/ui-app/src/app/pages/group/components/tabs/index.ts new file mode 100644 index 0000000000..cc13c0250e --- /dev/null +++ b/ui/ui-app/src/app/pages/group/components/tabs/index.ts @@ -0,0 +1 @@ +export * from "./GroupInfoTabContent.tsx"; diff --git a/ui/ui-app/src/app/pages/group/index.ts b/ui/ui-app/src/app/pages/group/index.ts new file mode 100644 index 0000000000..37cd6edb32 --- /dev/null +++ b/ui/ui-app/src/app/pages/group/index.ts @@ -0,0 +1,2 @@ +export * from "./components"; +export * from "./GroupPage.tsx"; diff --git a/ui/ui-app/src/app/pages/index.ts b/ui/ui-app/src/app/pages/index.ts index 655c5a5cb7..74dd477cea 100644 --- a/ui/ui-app/src/app/pages/index.ts +++ b/ui/ui-app/src/app/pages/index.ts @@ -1,7 +1,7 @@ export * from "./404"; -export * from "./artifact"; -export * from "./artifacts"; -export * from "./artifactVersion"; +export * from "./explore"; +export * from "./group"; +export * from "./version"; export * from "./settings"; export * from "./roles"; export * from "./root"; @@ -10,4 +10,4 @@ export * from "./PageDataLoader"; export * from "./PageError"; export * from "./PageErrorHandler"; export * from "./PageErrorType"; -export * from "./toPageError"; \ No newline at end of file +export * from "./toPageError"; diff --git a/ui/ui-app/src/app/pages/roles/RolesPage.tsx b/ui/ui-app/src/app/pages/roles/RolesPage.tsx index 8b49711912..8bb7eba804 100644 --- a/ui/ui-app/src/app/pages/roles/RolesPage.tsx +++ b/ui/ui-app/src/app/pages/roles/RolesPage.tsx @@ -17,7 +17,7 @@ import { GrantAccessModal } from "@app/pages/roles/components/modals/GrantAccess import { If, PleaseWaitModal } from "@apicurio/common-ui-components"; import { AdminService, useAdminService } from "@services/useAdminService.ts"; import { Principal } from "@services/useConfigService.ts"; -import { Paging } from "@services/useGroupsService.ts"; +import { Paging } from "@models/paging.model.ts"; export type RolesPageProps = { diff --git a/ui/ui-app/src/app/pages/roles/components/roleToolbar/RoleToolbar.tsx b/ui/ui-app/src/app/pages/roles/components/roleToolbar/RoleToolbar.tsx index 8421cdf795..6bce8232c2 100644 --- a/ui/ui-app/src/app/pages/roles/components/roleToolbar/RoleToolbar.tsx +++ b/ui/ui-app/src/app/pages/roles/components/roleToolbar/RoleToolbar.tsx @@ -14,8 +14,8 @@ import { ToolbarItem } from "@patternfly/react-core"; import { SearchIcon, SortAlphaDownAltIcon, SortAlphaDownIcon } from "@patternfly/react-icons"; -import { Paging } from "@services/useGroupsService.ts"; import { LoggerService, useLoggerService } from "@services/useLoggerService.ts"; +import { Paging } from "@models/paging.model.ts"; type FilterType = { diff --git a/ui/ui-app/src/app/pages/version/VersionPage.css b/ui/ui-app/src/app/pages/version/VersionPage.css new file mode 100644 index 0000000000..4acc94130a --- /dev/null +++ b/ui/ui-app/src/app/pages/version/VersionPage.css @@ -0,0 +1,36 @@ +div.artifact-page-tabs > ul > li.pf-c-tabs__item.pf-m-current > button.pf-c-tabs__button::before { + border-bottom-color: transparent; +} + +.artifact-details-main { + display: flex; + flex-direction: column; +} + +.artifact-details-main .pf-c-tab-content { + flex-grow: 1; + display: flex; + flex-direction: column; +} + +#artifact-page-tabs > ul > li:first-child { + margin-left: 20px; +} + +.ps_header-breadcrumbs { + padding: 12px 24px 0; +} + +.pf-v5-c-breadcrumb__item-divider { + color: #666; +} + +#pf-tab-section-content-artifact-page-tabs { + display: flex; + flex-grow: 1; + position: relative; +} + +.documentation-tab { + flex-grow: 1; +} \ No newline at end of file diff --git a/ui/ui-app/src/app/pages/version/VersionPage.tsx b/ui/ui-app/src/app/pages/version/VersionPage.tsx new file mode 100644 index 0000000000..8ec69053dc --- /dev/null +++ b/ui/ui-app/src/app/pages/version/VersionPage.tsx @@ -0,0 +1,296 @@ +import { FunctionComponent, useEffect, useState } from "react"; +import "./VersionPage.css"; +import { Breadcrumb, BreadcrumbItem, PageSection, PageSectionVariants, Tab, Tabs } from "@patternfly/react-core"; +import { Link, useParams } from "react-router-dom"; +import { ArtifactMetaData } from "@models/artifactMetaData.model.ts"; +import { + ContentTabContent, + DocumentationTabContent, + InfoTabContent, + PageDataLoader, + PageError, + PageErrorHandler, + toPageError, + VersionPageHeader +} from "@app/pages"; +import { ReferencesTabContent } from "@app/pages/version/components/tabs/ReferencesTabContent.tsx"; +import { ConfirmDeleteModal, EditMetaDataModal, IfFeature, MetaData } from "@app/components"; +import { ContentTypes } from "@models/contentTypes.model.ts"; +import { PleaseWaitModal } from "@apicurio/common-ui-components"; +import { AppNavigation, useAppNavigation } from "@services/useAppNavigation.ts"; +import { LoggerService, useLoggerService } from "@services/useLoggerService.ts"; +import { GroupsService, useGroupsService } from "@services/useGroupsService.ts"; +import { DownloadService, useDownloadService } from "@services/useDownloadService.ts"; +import { ArtifactTypes } from "@services/useArtifactTypesService.ts"; +import { VersionMetaData } from "@models/versionMetaData.model.ts"; + + +export type ArtifactVersionPageProps = { + // No properties +} + +/** + * The artifact version page. + */ +export const VersionPage: FunctionComponent = () => { + const [pageError, setPageError] = useState(); + const [loaders, setLoaders] = useState | Promise[] | undefined>(); + const [activeTabKey, setActiveTabKey] = useState("overview"); + const [artifact, setArtifact] = useState(); + const [artifactVersion, setArtifactVersion] = useState(); + const [versionContent, setArtifactContent] = useState(""); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isPleaseWaitModalOpen, setIsPleaseWaitModalOpen] = useState(false); + const [pleaseWaitMessage, setPleaseWaitMessage] = useState(""); + + const appNavigation: AppNavigation = useAppNavigation(); + const logger: LoggerService = useLoggerService(); + const groups: GroupsService = useGroupsService(); + const download: DownloadService = useDownloadService(); + const { groupId, artifactId, version }= useParams(); + + const is404 = (e: any) => { + if (typeof e === "string") { + try { + const eo: any = JSON.parse(e); + if (eo && eo.error_code && eo.error_code === 404) { + return true; + } + } catch (e) { + // Do nothing + } + } + return false; + }; + + const createLoaders = (): Promise[] => { + let gid: string|null = groupId as string; + if (gid == "default") { + gid = null; + } + logger.info("Loading data for artifact: ", artifactId); + return [ + groups.getArtifactMetaData(gid, artifactId as string) + .then(setArtifact) + .catch(error => { + setPageError(toPageError(error, "Error loading page data.")); + }), + groups.getArtifactVersionMetaData(gid, artifactId as string, version as string) + .then(setArtifactVersion) + .catch(error => { + setPageError(toPageError(error, "Error loading page data.")); + }), + groups.getArtifactVersionContent(gid, artifactId as string, version as string) + .then(setArtifactContent) + .catch(e => { + logger.warn("Failed to get artifact content: ", e); + if (is404(e)) { + setArtifactContent("Artifact version content not available (404 Not Found)."); + } else { + const pageError: PageError = toPageError(e, "Error loading page data."); + setPageError(pageError); + } + }), + ]; + }; + + const handleTabClick = (_event: any, tabIndex: any): void => { + setActiveTabKey(tabIndex); + }; + + const onDeleteVersion = (): void => { + setIsDeleteModalOpen(true); + }; + + const showDocumentationTab = (): boolean => { + return artifact?.type === "OPENAPI" && artifactVersion?.state !== "DISABLED"; + }; + + const doDownloadVersion = (): void => { + const content: string = versionContent; + + let contentType: string = ContentTypes.APPLICATION_JSON; + let fext: string = "json"; + if (artifact?.type === ArtifactTypes.PROTOBUF) { + contentType = ContentTypes.APPLICATION_PROTOBUF; + fext = "proto"; + } + if (artifact?.type === ArtifactTypes.WSDL) { + contentType = ContentTypes.APPLICATION_XML; + fext = "wsdl"; + } + if (artifact?.type === ArtifactTypes.XSD) { + contentType = ContentTypes.APPLICATION_XML; + fext = "xsd"; + } + if (artifact?.type === ArtifactTypes.XML) { + contentType = ContentTypes.APPLICATION_XML; + fext = "xml"; + } + if (artifact?.type === ArtifactTypes.GRAPHQL) { + contentType = ContentTypes.APPLICATION_JSON; + fext = "graphql"; + } + + const fname: string = nameOrId() + "." + fext; + download.downloadToFS(content, contentType, fname).catch(error => { + setPageError(toPageError(error, "Error downloading artifact content.")); + }); + }; + + const nameOrId = (): string => { + return artifact?.name || artifact?.artifactId || ""; + }; + + const versionName = (): string => { + return artifactVersion?.name || ""; + }; + + const versionDescription = (): string => { + return artifactVersion?.description || ""; + }; + + const versionLabels = (): { [key: string]: string } => { + return artifactVersion?.labels || {}; + }; + + const onDeleteModalClose = (): void => { + setIsDeleteModalOpen(false); + }; + + const doDeleteVersion = (): void => { + onDeleteModalClose(); + pleaseWait(true, "Deleting version, please wait..."); + groups.deleteArtifactVersion(groupId as string, artifactId as string, version as string).then( () => { + pleaseWait(false); + const gid = encodeURIComponent(groupId || "default"); + const aid: string = encodeURIComponent(artifactId as string); + appNavigation.navigateTo(`/explore/${gid}/${aid}`); + }).catch(error => { + setPageError(toPageError(error, "Error deleting a version.")); + }); + }; + + const openEditMetaDataModal = (): void => { + setIsEditModalOpen(true); + }; + + const onEditModalClose = (): void => { + setIsEditModalOpen(false); + }; + + const doEditMetaData = (metaData: MetaData): void => { + groups.updateArtifactVersionMetaData(groupId as string, artifactId as string, version as string, metaData).then( () => { + if (artifact) { + setArtifactVersion({ + ...artifactVersion, + ...metaData + } as VersionMetaData); + } + }).catch( error => { + setPageError(toPageError(error, "Error editing artifact metadata.")); + }); + onEditModalClose(); + }; + + const pleaseWait = (isOpen: boolean, message: string = ""): void => { + setIsPleaseWaitModalOpen(isOpen); + setPleaseWaitMessage(message); + }; + + useEffect(() => { + setLoaders(createLoaders()); + }, [groupId, artifactId, version]); + + const tabs: any[] = [ + + + , + + + , + + + , + + + , + ]; + if (!showDocumentationTab()) { + tabs.splice(1, 1); + } + + const gid: string = groupId || "default"; + const hasGroup: boolean = gid != "default"; + let breadcrumbs = ( + + Explore + { gid } + { artifactId } + { version as string } + + ); + if (!hasGroup) { + breadcrumbs = ( + + Explore + { artifactId } + { version as string } + + ); + } + + return ( + + + + + + + + + + + + + + + + + ); + +}; diff --git a/ui/ui-app/src/app/pages/version/components/index.ts b/ui/ui-app/src/app/pages/version/components/index.ts new file mode 100644 index 0000000000..cc8a7498bd --- /dev/null +++ b/ui/ui-app/src/app/pages/version/components/index.ts @@ -0,0 +1,2 @@ +export * from "./pageheader"; +export * from "./tabs"; diff --git a/ui/ui-app/src/app/pages/version/components/pageheader/VersionPageHeader.css b/ui/ui-app/src/app/pages/version/components/pageheader/VersionPageHeader.css new file mode 100644 index 0000000000..b87bd8c237 --- /dev/null +++ b/ui/ui-app/src/app/pages/version/components/pageheader/VersionPageHeader.css @@ -0,0 +1,7 @@ +#upload-version-button { +} + +#delete-artifact-button { + margin-left: 10px; + margin-right: 10px; +} diff --git a/ui/ui-app/src/app/pages/version/components/pageheader/VersionPageHeader.tsx b/ui/ui-app/src/app/pages/version/components/pageheader/VersionPageHeader.tsx new file mode 100644 index 0000000000..37440b33bd --- /dev/null +++ b/ui/ui-app/src/app/pages/version/components/pageheader/VersionPageHeader.tsx @@ -0,0 +1,50 @@ +import { FunctionComponent } from "react"; +import "./VersionPageHeader.css"; +import { Button, Flex, FlexItem, Text, TextContent, TextVariants } from "@patternfly/react-core"; +import { IfAuth, IfFeature } from "@app/components"; +import { If } from "@apicurio/common-ui-components"; + + +/** + * Properties + */ +export type ArtifactVersionPageHeaderProps = { + groupId: string; + artifactId: string; + version: string; + onDelete: () => void; + onDownload: () => void; +}; + +/** + * Models the page header for the Artifact page. + */ +export const VersionPageHeader: FunctionComponent = (props: ArtifactVersionPageHeaderProps) => { + return ( + + + + + + {props.groupId} + / + + {props.artifactId} + / + {props.version} + + + + + + + + + + + + + ); +}; diff --git a/ui/ui-app/src/app/pages/version/components/pageheader/index.ts b/ui/ui-app/src/app/pages/version/components/pageheader/index.ts new file mode 100644 index 0000000000..05194e9bd0 --- /dev/null +++ b/ui/ui-app/src/app/pages/version/components/pageheader/index.ts @@ -0,0 +1 @@ +export * from "./VersionPageHeader"; diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ContentTabContent.css b/ui/ui-app/src/app/pages/version/components/tabs/ContentTabContent.css similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/ContentTabContent.css rename to ui/ui-app/src/app/pages/version/components/tabs/ContentTabContent.css diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ContentTabContent.tsx b/ui/ui-app/src/app/pages/version/components/tabs/ContentTabContent.tsx similarity index 95% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/ContentTabContent.tsx rename to ui/ui-app/src/app/pages/version/components/tabs/ContentTabContent.tsx index 070a834675..f2c8e95526 100644 --- a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ContentTabContent.tsx +++ b/ui/ui-app/src/app/pages/version/components/tabs/ContentTabContent.tsx @@ -36,7 +36,7 @@ const formatContent = (artifactContent: string): string => { * Properties */ export type ContentTabContentProps = { - artifactContent: string; + versionContent: string; artifactType: string; }; @@ -45,7 +45,7 @@ export type ContentTabContentProps = { * Models the content of the Artifact Content tab. */ export const ContentTabContent: FunctionComponent = (props: ContentTabContentProps) => { - const [content, setContent] = useState(formatContent(props.artifactContent)); + const [content, setContent] = useState(formatContent(props.versionContent)); const [editorMode, setEditorMode] = useState(getEditorMode(props.artifactType)); const [compactButtons, setCompactButtons] = useState(false); @@ -63,9 +63,9 @@ export const ContentTabContent: FunctionComponent = (pro let content: string = `Error formatting code to: ${mode}`; try { if (mode === "yaml") { - content = YAML.stringify(JSON.parse(props.artifactContent), null, 4); + content = YAML.stringify(JSON.parse(props.versionContent), null, 4); } else { - content = JSON.stringify(YAML.parse(props.artifactContent), null, 2); + content = JSON.stringify(YAML.parse(props.versionContent), null, 2); } } catch (e) { handleInvalidContentError(e); diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/DocumentationTabContent.tsx b/ui/ui-app/src/app/pages/version/components/tabs/DocumentationTabContent.tsx similarity index 93% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/DocumentationTabContent.tsx rename to ui/ui-app/src/app/pages/version/components/tabs/DocumentationTabContent.tsx index e33865098b..d13c8b1025 100644 --- a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/DocumentationTabContent.tsx +++ b/ui/ui-app/src/app/pages/version/components/tabs/DocumentationTabContent.tsx @@ -2,7 +2,7 @@ import { FunctionComponent, useState } from "react"; import { ErrorTabContent } from "@app/pages"; import { If } from "@apicurio/common-ui-components"; import YAML from "yaml"; -import { AsyncApiVisualizer, OpenApiVisualizer } from "@app/pages/artifactVersion/components/tabs/visualizers"; +import { AsyncApiVisualizer, OpenApiVisualizer } from "@app/pages/version/components/tabs/visualizers"; import { ArtifactTypes } from "@services/useArtifactTypesService.ts"; enum VisualizerType { @@ -40,7 +40,7 @@ const parseContent = (artifactContent: string): any => { * Properties */ export type DocumentationTabContentProps = { - artifactContent: string; + versionContent: string; artifactType: string; }; @@ -49,7 +49,7 @@ export type DocumentationTabContentProps = { * Models the content of the Documentation tab on the artifact details page. */ export const DocumentationTabContent: FunctionComponent = (props: DocumentationTabContentProps) => { - const [parsedContent] = useState(parseContent(props.artifactContent)); + const [parsedContent] = useState(parseContent(props.versionContent)); const [visualizerType] = useState(getVisualizerType(props.artifactType)); const [error] = useState(); diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ErrorTabContentState.css b/ui/ui-app/src/app/pages/version/components/tabs/ErrorTabContentState.css similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/ErrorTabContentState.css rename to ui/ui-app/src/app/pages/version/components/tabs/ErrorTabContentState.css diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ErrorTabContentState.tsx b/ui/ui-app/src/app/pages/version/components/tabs/ErrorTabContentState.tsx similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/ErrorTabContentState.tsx rename to ui/ui-app/src/app/pages/version/components/tabs/ErrorTabContentState.tsx diff --git a/ui/ui-app/src/app/pages/version/components/tabs/InfoTabContent.css b/ui/ui-app/src/app/pages/version/components/tabs/InfoTabContent.css new file mode 100644 index 0000000000..aa21581834 --- /dev/null +++ b/ui/ui-app/src/app/pages/version/components/tabs/InfoTabContent.css @@ -0,0 +1,55 @@ +.overview-tab-content { + padding: 22px; + background-color: rgb(240,240,240); +} + +.overview-tab-content .version-basics { + width: 50%; +} + +@media only screen and (max-width: 1400px) { + .overview-tab-content .version-basics { + width: 60%; + } +} + +@media only screen and (max-width: 1024px) { + .overview-tab-content .version-basics { + width: 75%; + } +} + +@media only screen and (max-width: 800px) { + .overview-tab-content .version-basics { + width: 100%; + } +} + +.overview-tab-content .version-basics .title-and-type { +} + +.overview-tab-content .version-basics .title-and-type .type { +} + +.overview-tab-content .version-basics .title-and-type .title { + font-size: 17px; + font-weight: bold; + line-height: 24px; + display: inline-block; +} + +.overview-tab-content .version-basics .metaData { + margin-bottom: 20px; +} + +.overview-tab-content .version-basics .actions { +} + + +.label-truncate, .label-truncate, .label-truncate > span, .label-truncate > span { + min-width: unset; +} + +#generate-client-action { + margin-left: 10px; +} diff --git a/ui/ui-app/src/app/pages/version/components/tabs/InfoTabContent.tsx b/ui/ui-app/src/app/pages/version/components/tabs/InfoTabContent.tsx new file mode 100644 index 0000000000..cc224fac0c --- /dev/null +++ b/ui/ui-app/src/app/pages/version/components/tabs/InfoTabContent.tsx @@ -0,0 +1,140 @@ +import { FunctionComponent } from "react"; +import "./InfoTabContent.css"; +import "@app/styles/empty.css"; +import { ArtifactTypeIcon, IfAuth, IfFeature } from "@app/components"; +import { + Button, + Card, + CardBody, + CardTitle, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Divider, + Flex, + FlexItem, + Label, + Truncate +} from "@patternfly/react-core"; +import { PencilAltIcon } from "@patternfly/react-icons"; +import { FromNow, If } from "@apicurio/common-ui-components"; +import { VersionMetaData } from "@models/versionMetaData.model.ts"; +import { ArtifactMetaData } from "@models/artifactMetaData.model.ts"; + +/** + * Properties + */ +export type InfoTabContentProps = { + artifact: ArtifactMetaData; + version: VersionMetaData; + onEditMetaData: () => void; +}; + +/** + * Models the content of the Version Info (overview) tab. + */ +export const InfoTabContent: FunctionComponent = (props: InfoTabContentProps) => { + + const description = (): string => { + return props.version.description || "No description"; + }; + + const artifactName = (): string => { + return props.version.name || "No name"; + }; + + return ( +
+
+ + +
+ + + Version metadata + + + + + + + + +
+
+ + + + + Name + + { artifactName() } + + + + Description + + { description() } + + + + Status + {props.version.state} + + + Created + + + + + + + Owner + + {props.version.owner} + + + + + Modified + + + + + + Global ID + {props.version.globalId} + + + Content ID + {props.version.contentId} + + + Labels + {!props.version.labels || !Object.keys(props.version.labels).length ? + No labels : + {Object.entries(props.version.labels).map(([key, value]) => + + )} + } + + + +
+
+
+ ); + +}; diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferenceList.tsx b/ui/ui-app/src/app/pages/version/components/tabs/ReferenceList.tsx similarity index 95% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferenceList.tsx rename to ui/ui-app/src/app/pages/version/components/tabs/ReferenceList.tsx index c26cae10c8..613c2f3bcf 100644 --- a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferenceList.tsx +++ b/ui/ui-app/src/app/pages/version/components/tabs/ReferenceList.tsx @@ -51,7 +51,7 @@ export const ReferenceList: FunctionComponent = ( // ID if (colIndex === 2) { const groupId: string = column.groupId == null ? "default" : column.groupId; - const link: string = `/artifacts/${ encodeURIComponent(groupId)}/${ encodeURIComponent(column.artifactId) }/versions/${ encodeURIComponent(column.version) }`; + const link: string = `/explore/${ encodeURIComponent(groupId)}/${ encodeURIComponent(column.artifactId) }/${ encodeURIComponent(column.version) }`; return ( { column.artifactId } ); diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferencesTabContent.css b/ui/ui-app/src/app/pages/version/components/tabs/ReferencesTabContent.css similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferencesTabContent.css rename to ui/ui-app/src/app/pages/version/components/tabs/ReferencesTabContent.css diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferencesTabContent.tsx b/ui/ui-app/src/app/pages/version/components/tabs/ReferencesTabContent.tsx similarity index 96% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferencesTabContent.tsx rename to ui/ui-app/src/app/pages/version/components/tabs/ReferencesTabContent.tsx index c4aefa8a5e..d1561bde7f 100644 --- a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferencesTabContent.tsx +++ b/ui/ui-app/src/app/pages/version/components/tabs/ReferencesTabContent.tsx @@ -6,12 +6,13 @@ import { ArtifactReference } from "@models/artifactReference.model.ts"; import { ReferencesToolbar, ReferencesToolbarFilterCriteria -} from "@app/pages/artifactVersion/components/tabs/ReferencesToolbar.tsx"; +} from "@app/pages/version/components/tabs/ReferencesToolbar.tsx"; import { ReferenceType } from "@models/referenceType.ts"; import { ListWithToolbar } from "@apicurio/common-ui-components"; -import { GroupsService, Paging, useGroupsService } from "@services/useGroupsService.ts"; +import { GroupsService, useGroupsService } from "@services/useGroupsService.ts"; import { LoggerService, useLoggerService } from "@services/useLoggerService.ts"; import { VersionMetaData } from "@models/versionMetaData.model.ts"; +import { Paging } from "@models/paging.model.ts"; /** * Properties diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferencesToolbar.css b/ui/ui-app/src/app/pages/version/components/tabs/ReferencesToolbar.css similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferencesToolbar.css rename to ui/ui-app/src/app/pages/version/components/tabs/ReferencesToolbar.css diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferencesToolbar.tsx b/ui/ui-app/src/app/pages/version/components/tabs/ReferencesToolbar.tsx similarity index 98% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferencesToolbar.tsx rename to ui/ui-app/src/app/pages/version/components/tabs/ReferencesToolbar.tsx index f8413b428d..ae7a8074d8 100644 --- a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/ReferencesToolbar.tsx +++ b/ui/ui-app/src/app/pages/version/components/tabs/ReferencesToolbar.tsx @@ -17,7 +17,7 @@ import { OnPerPageSelect, OnSetPage } from "@patternfly/react-core/dist/js/compo import { ArtifactReference } from "@models/artifactReference.model.ts"; import { ReferenceType } from "@models/referenceType.ts"; import { ObjectSelect } from "@apicurio/common-ui-components"; -import { Paging } from "@services/useGroupsService.ts"; +import { Paging } from "@models/paging.model.ts"; export interface ReferencesToolbarFilterCriteria { filterSelection: string; diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/index.ts b/ui/ui-app/src/app/pages/version/components/tabs/index.ts similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/index.ts rename to ui/ui-app/src/app/pages/version/components/tabs/index.ts diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/visualizers/AsyncApiVisualizer.tsx b/ui/ui-app/src/app/pages/version/components/tabs/visualizers/AsyncApiVisualizer.tsx similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/visualizers/AsyncApiVisualizer.tsx rename to ui/ui-app/src/app/pages/version/components/tabs/visualizers/AsyncApiVisualizer.tsx diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/visualizers/OpenApiVisualizer.tsx b/ui/ui-app/src/app/pages/version/components/tabs/visualizers/OpenApiVisualizer.tsx similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/visualizers/OpenApiVisualizer.tsx rename to ui/ui-app/src/app/pages/version/components/tabs/visualizers/OpenApiVisualizer.tsx diff --git a/ui/ui-app/src/app/pages/artifactVersion/components/tabs/visualizers/index.ts b/ui/ui-app/src/app/pages/version/components/tabs/visualizers/index.ts similarity index 100% rename from ui/ui-app/src/app/pages/artifactVersion/components/tabs/visualizers/index.ts rename to ui/ui-app/src/app/pages/version/components/tabs/visualizers/index.ts diff --git a/ui/ui-app/src/app/pages/version/index.ts b/ui/ui-app/src/app/pages/version/index.ts new file mode 100644 index 0000000000..0cc95569d5 --- /dev/null +++ b/ui/ui-app/src/app/pages/version/index.ts @@ -0,0 +1,2 @@ +export * from "./components"; +export * from "./VersionPage.tsx"; diff --git a/ui/ui-app/src/models/artifactMetaData.model.ts b/ui/ui-app/src/models/artifactMetaData.model.ts index 944681cc70..86408d7b79 100644 --- a/ui/ui-app/src/models/artifactMetaData.model.ts +++ b/ui/ui-app/src/models/artifactMetaData.model.ts @@ -5,7 +5,7 @@ export interface ArtifactMetaData { artifactId: string; name: string|null; description: string|null; - labels: { [key: string]: string }; + labels: { [key: string]: string | undefined }; type: string; version: string; owner: string; diff --git a/ui/ui-app/src/models/artifactSearchResults.model.ts b/ui/ui-app/src/models/artifactSearchResults.model.ts new file mode 100644 index 0000000000..9ee81b3b47 --- /dev/null +++ b/ui/ui-app/src/models/artifactSearchResults.model.ts @@ -0,0 +1,6 @@ +import { SearchedArtifact } from "@models/searchedArtifact.model.ts"; + +export interface ArtifactSearchResults { + artifacts: SearchedArtifact[]; + count: number; +} diff --git a/ui/ui-app/src/models/artifactSortBy.model.ts b/ui/ui-app/src/models/artifactSortBy.model.ts new file mode 100644 index 0000000000..0b22d9ded3 --- /dev/null +++ b/ui/ui-app/src/models/artifactSortBy.model.ts @@ -0,0 +1,8 @@ + +export enum ArtifactSortBy { + name = "name", + artifactId = "artifactId", + createdOn = "createdOn", + modifiedOn = "modifiedOn", + artifactType = "artifactType", +} diff --git a/ui/ui-app/src/models/contentTypes.model.ts b/ui/ui-app/src/models/contentTypes.model.ts index f11523bb93..4c9c421b74 100644 --- a/ui/ui-app/src/models/contentTypes.model.ts +++ b/ui/ui-app/src/models/contentTypes.model.ts @@ -5,5 +5,6 @@ export class ContentTypes { public static APPLICATION_XML: string = "application/xml"; public static APPLICATION_PROTOBUF: string = "application/x-protobuf"; public static APPLICATION_GRAPHQL: string = "application/graphql"; + public static APPLICATION_OCTET_STREAM: string = "application/octet-stream"; } diff --git a/ui/ui-app/src/models/createGroup.model.ts b/ui/ui-app/src/models/createGroup.model.ts new file mode 100644 index 0000000000..5bf2e7fa93 --- /dev/null +++ b/ui/ui-app/src/models/createGroup.model.ts @@ -0,0 +1,5 @@ +export interface CreateGroup { + groupId: string; + description?: string; + labels?: { [key: string]: string }; +} diff --git a/ui/ui-app/src/models/editableArtifactMetaData.model.ts b/ui/ui-app/src/models/editableArtifactMetaData.model.ts new file mode 100644 index 0000000000..db82b66772 --- /dev/null +++ b/ui/ui-app/src/models/editableArtifactMetaData.model.ts @@ -0,0 +1,7 @@ + +export interface EditableArtifactMetaData { + name?: string; + description?: string; + labels?: { [key: string]: string|undefined }; + owner?: string; +} diff --git a/ui/ui-app/src/models/editableGroupMetaData.model.ts b/ui/ui-app/src/models/editableGroupMetaData.model.ts new file mode 100644 index 0000000000..f9e567fd14 --- /dev/null +++ b/ui/ui-app/src/models/editableGroupMetaData.model.ts @@ -0,0 +1,5 @@ + +export interface EditableGroupMetaData { + description: string; + labels: { [key: string]: string|undefined }; +} diff --git a/ui/ui-app/src/models/editableVersionMetaData.model.ts b/ui/ui-app/src/models/editableVersionMetaData.model.ts new file mode 100644 index 0000000000..1c0c504d55 --- /dev/null +++ b/ui/ui-app/src/models/editableVersionMetaData.model.ts @@ -0,0 +1,8 @@ +import { VersionState } from "@models/versionState.model.ts"; + +export interface EditableVersionMetaData { + name?: string; + description?: string; + labels?: { [key: string]: string|undefined }; + state?: VersionState; +} diff --git a/ui/ui-app/src/models/groupMetaData.model.ts b/ui/ui-app/src/models/groupMetaData.model.ts new file mode 100644 index 0000000000..8973518ff2 --- /dev/null +++ b/ui/ui-app/src/models/groupMetaData.model.ts @@ -0,0 +1,12 @@ + +export interface GroupMetaData { + + groupId: string; + description?: string; + labels?: { [key: string]: string | undefined }; + owner: string; + createdOn: string; + modifiedBy: string; + modifiedOn: string; + +} diff --git a/ui/ui-app/src/models/groupSearchResults.model.ts b/ui/ui-app/src/models/groupSearchResults.model.ts new file mode 100644 index 0000000000..8db10a4cf3 --- /dev/null +++ b/ui/ui-app/src/models/groupSearchResults.model.ts @@ -0,0 +1,8 @@ +import { SearchedGroup } from "@models/searchedGroup.model.ts"; + +export interface GroupSearchResults { + + groups: SearchedGroup[]; + count: number; + +} diff --git a/ui/ui-app/src/models/groupSortBy.model.ts b/ui/ui-app/src/models/groupSortBy.model.ts new file mode 100644 index 0000000000..1f94e5657c --- /dev/null +++ b/ui/ui-app/src/models/groupSortBy.model.ts @@ -0,0 +1,5 @@ + +export enum GroupSortBy { + groupId = "groupId", + createdOn = "createdOn", +} diff --git a/ui/ui-app/src/models/index.ts b/ui/ui-app/src/models/index.ts index cb1a153399..2e0840cb77 100644 --- a/ui/ui-app/src/models/index.ts +++ b/ui/ui-app/src/models/index.ts @@ -17,3 +17,16 @@ export * from "./rule.model"; export * from "./artifactMetaData.model"; export * from "./artifactReference.model"; export * from "./userInfo.model"; +export * from "./editableArtifactMetaData.model"; +export * from "./editableGroupMetaData.model"; +export * from "./groupSearchResults.model"; +export * from "./artifactSearchResults.model"; +export * from "./searchedGroup.model"; +export * from "./createGroup.model"; +export * from "./paging.model"; +export * from "./versionState.model"; +export * from "./groupSortBy.model"; +export * from "./artifactSortBy.model"; +export * from "./versionSortBy.model"; +export * from "./sortOrder.model"; +export * from "./versionSearchResults.model"; diff --git a/ui/ui-app/src/models/paging.model.ts b/ui/ui-app/src/models/paging.model.ts new file mode 100644 index 0000000000..b179f27209 --- /dev/null +++ b/ui/ui-app/src/models/paging.model.ts @@ -0,0 +1,4 @@ +export interface Paging { + page: number; + pageSize: number; +} diff --git a/ui/ui-app/src/models/searchedGroup.model.ts b/ui/ui-app/src/models/searchedGroup.model.ts new file mode 100644 index 0000000000..52726b2dfc --- /dev/null +++ b/ui/ui-app/src/models/searchedGroup.model.ts @@ -0,0 +1,11 @@ +export interface SearchedGroup { + + groupId: string|null; + description: string; + labels: string[]; + createdOn: Date; + owner: string; + modifiedOn: Date; + modifiedBy: string; + +} diff --git a/ui/ui-app/src/models/searchedVersion.model.ts b/ui/ui-app/src/models/searchedVersion.model.ts index ea41bf29c0..146d92fae3 100644 --- a/ui/ui-app/src/models/searchedVersion.model.ts +++ b/ui/ui-app/src/models/searchedVersion.model.ts @@ -2,12 +2,11 @@ export interface SearchedVersion { globalId: number; contentId: number|null; - version: number; + version: string; type: string; state: string; name: string; description: string; - labels: string[]; createdOn: string; owner: string; diff --git a/ui/ui-app/src/models/sortOrder.model.ts b/ui/ui-app/src/models/sortOrder.model.ts new file mode 100644 index 0000000000..9c06f04462 --- /dev/null +++ b/ui/ui-app/src/models/sortOrder.model.ts @@ -0,0 +1,5 @@ + +export enum SortOrder { + asc = "asc", + desc = "desc", +} diff --git a/ui/ui-app/src/models/versionSearchResults.model.ts b/ui/ui-app/src/models/versionSearchResults.model.ts new file mode 100644 index 0000000000..f8af47d6ac --- /dev/null +++ b/ui/ui-app/src/models/versionSearchResults.model.ts @@ -0,0 +1,6 @@ +import { SearchedVersion } from "@models/searchedVersion.model.ts"; + +export interface VersionSearchResults { + versions: SearchedVersion[]; + count: number; +} diff --git a/ui/ui-app/src/models/versionSortBy.model.ts b/ui/ui-app/src/models/versionSortBy.model.ts new file mode 100644 index 0000000000..b6815ed4ad --- /dev/null +++ b/ui/ui-app/src/models/versionSortBy.model.ts @@ -0,0 +1,7 @@ + +export enum VersionSortBy { + name = "name", + version = "version", + createdOn = "createdOn", + globalId = "globalId", +} diff --git a/ui/ui-app/src/models/versionState.model.ts b/ui/ui-app/src/models/versionState.model.ts new file mode 100644 index 0000000000..2ae17122e4 --- /dev/null +++ b/ui/ui-app/src/models/versionState.model.ts @@ -0,0 +1,5 @@ +export enum VersionState { + + ENABLED = "ENABLED", DISABLED = "DISABLED", DEPRECATED = "DEPRECATED" + +} diff --git a/ui/ui-app/src/services/index.ts b/ui/ui-app/src/services/index.ts index 07caf380ac..25d1878c1f 100644 --- a/ui/ui-app/src/services/index.ts +++ b/ui/ui-app/src/services/index.ts @@ -10,3 +10,4 @@ export * from "./useSystemService"; export * from "./useUrlService"; export * from "./useUserService"; export * from "./useVersionService"; +export * from "./useSearchService"; diff --git a/ui/ui-app/src/services/useAdminService.ts b/ui/ui-app/src/services/useAdminService.ts index 7e7c818ec4..0018d7bbe4 100644 --- a/ui/ui-app/src/services/useAdminService.ts +++ b/ui/ui-app/src/services/useAdminService.ts @@ -14,8 +14,8 @@ import { httpPostWithReturn, httpPut, httpPutWithReturn } from "@utils/rest.utils.ts"; -import { Paging } from "@services/useGroupsService.ts"; import { RoleMappingSearchResults } from "@models/roleMappingSearchResults.model.ts"; +import { Paging } from "@models/paging.model.ts"; const getArtifactTypes = async (config: ConfigService, auth: AuthService): Promise => { @@ -171,7 +171,7 @@ const importFrom = async (config: ConfigService, auth: AuthService, file: string const options = await createAuthOptions(auth); options.headers = { ...options.headers, - "Accept": "application/zip" + "Content-Type": "application/zip" }; const endpoint: string = createEndpoint(baseHref, "/admin/import"); return httpPost(endpoint, file, options,undefined, progressFunction); diff --git a/ui/ui-app/src/services/useGroupsService.ts b/ui/ui-app/src/services/useGroupsService.ts index 87d39ca381..3c85f6242e 100644 --- a/ui/ui-app/src/services/useGroupsService.ts +++ b/ui/ui-app/src/services/useGroupsService.ts @@ -8,59 +8,28 @@ import { httpPut, httpPutWithReturn } from "@utils/rest.utils.ts"; -import { SearchedArtifact } from "@models/searchedArtifact.model.ts"; import { ArtifactMetaData } from "@models/artifactMetaData.model.ts"; import { VersionMetaData } from "@models/versionMetaData.model.ts"; import { ReferenceType } from "@models/referenceType.ts"; import { ArtifactReference } from "@models/artifactReference.model.ts"; -import { SearchedVersion } from "@models/searchedVersion.model.ts"; import { Rule } from "@models/rule.model.ts"; import { AuthService, useAuth } from "@apicurio/common-ui-components"; -import { contentType } from "@utils/content.utils.ts"; import { CreateArtifact } from "@models/createArtifact.model.ts"; import { CreateArtifactResponse } from "@models/createArtifactResponse.model.ts"; import { CreateVersion } from "@models/createVersion.model.ts"; +import { EditableVersionMetaData } from "@models/editableVersionMetaData.model.ts"; +import { EditableArtifactMetaData } from "@models/editableArtifactMetaData.model.ts"; +import { GroupMetaData } from "@models/groupMetaData.model.ts"; +import { CreateGroup } from "@models/createGroup.model.ts"; +import { EditableGroupMetaData } from "@models/editableGroupMetaData.model.ts"; +import { ArtifactSearchResults } from "@models/artifactSearchResults.model.ts"; +import { Paging } from "@models/paging.model.ts"; +import { SortOrder } from "@models/sortOrder.model.ts"; +import { ArtifactSortBy } from "@models/artifactSortBy.model.ts"; +import { VersionSortBy } from "@models/versionSortBy.model.ts"; +import { VersionSearchResults } from "@models/versionSearchResults.model.ts"; -export interface CreateArtifactData { - groupId: string; - id: string|null; - type: string; - fromURL?: string|null; - sha?: string|null; - content?: string|null; - contentType?: string|null; -} - -export interface CreateVersionData { - type: string; - content: string; -} - -export interface GetArtifactsCriteria { - type: string; - value: string; - sortAscending: boolean; -} - -export interface Paging { - page: number; - pageSize: number; -} - -export interface ArtifactsSearchResults { - artifacts: SearchedArtifact[]; - count: number; - page: number; - pageSize: number; -} - -export interface EditableMetaData { - name: string; - description: string; - labels: { [key: string]: string|undefined }; -} - export interface ClientGeneration { clientClassName: string; namespaceName: string; @@ -70,74 +39,84 @@ export interface ClientGeneration { content: string; } - -const createArtifact = async (config: ConfigService, auth: AuthService, data: CreateArtifactData): Promise => { +const createGroup = async (config: ConfigService, auth: AuthService, data: CreateGroup): Promise => { const baseHref: string = config.artifactsUrl(); - const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts", { groupId: data.groupId }); + const endpoint: string = createEndpoint(baseHref, "/groups", { groupId: data.groupId }); const options = await createAuthOptions(auth); - const body: CreateArtifact = { - artifactId: data.id ? data.id : undefined, - type: data.type, - firstVersion: { - content: { - content: data.content as string, - contentType: data.contentType ? data.contentType : "application/json", - } - } - }; - - return httpPostWithReturn(endpoint, body, options); + return httpPostWithReturn(endpoint, data, options); }; -const createArtifactVersion = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string, data: CreateVersionData): Promise => { +const getGroupMetaData = async (config: ConfigService, auth: AuthService, groupId: string): Promise => { groupId = normalizeGroupId(groupId); const baseHref: string = config.artifactsUrl(); + const endpoint: string = createEndpoint(baseHref, "/groups/:groupId", { groupId }); const options = await createAuthOptions(auth); - const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/versions", { groupId, artifactId }); - const ct: string = contentType(data.type, data.content); - const createVersion: CreateVersion = { - content: { - content: data.content, - contentType: ct - } - }; - - return httpPostWithReturn(endpoint, createVersion, options); + return httpGet(endpoint, options); }; -const getArtifacts = async (config: ConfigService, auth: AuthService, criteria: GetArtifactsCriteria, paging: Paging): Promise => { - console.debug("[GroupsService] Getting artifacts: ", criteria, paging); +const getGroupArtifacts = async (config: ConfigService, auth: AuthService, groupId: string, sortBy: ArtifactSortBy, sortOrder: SortOrder, paging: Paging): Promise => { + groupId = normalizeGroupId(groupId); const start: number = (paging.page - 1) * paging.pageSize; const end: number = start + paging.pageSize; const queryParams: any = { limit: end, offset: start, - order: criteria.sortAscending ? "asc" : "desc", - orderby: "name" + order: sortOrder, + orderby: sortBy }; - if (criteria.value) { - if (criteria.type == "everything") { - queryParams["name"] = criteria.value; - queryParams["description"] = criteria.value; - queryParams["labels"] = criteria.value; - } else { - queryParams[criteria.type] = criteria.value; - } - } + const baseHref: string = config.artifactsUrl(); - const endpoint: string = createEndpoint(baseHref, "/search/artifacts", {}, queryParams); + const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts", { groupId }, queryParams); const options = await createAuthOptions(auth); - return httpGet(endpoint, options, (data) => { - const results: ArtifactsSearchResults = { - artifacts: data.artifacts, - count: data.count, - page: paging.page, - pageSize: paging.pageSize - }; - return results; - }); + return httpGet(endpoint, options); +}; + +const updateGroupMetaData = async (config: ConfigService, auth: AuthService, groupId: string, metaData: EditableGroupMetaData): Promise => { + groupId = normalizeGroupId(groupId); + + const baseHref: string = config.artifactsUrl(); + const endpoint: string = createEndpoint(baseHref, "/groups/:groupId", { groupId }); + const options = await createAuthOptions(auth); + return httpPut(endpoint, metaData, options); +}; + +const updateGroupOwner = async (config: ConfigService, auth: AuthService, groupId: string, newOwner: string): Promise => { + return updateGroupMetaData(config, auth, groupId, { + owner: newOwner + } as any); +}; + +const deleteGroup = async (config: ConfigService, auth: AuthService, groupId: string): Promise => { + groupId = normalizeGroupId(groupId); + + console.info("[GroupsService] Deleting group:", groupId); + const baseHref: string = config.artifactsUrl(); + const endpoint: string = createEndpoint(baseHref, "/groups/:groupId", { groupId }); + const options = await createAuthOptions(auth); + return httpDelete(endpoint, options); +}; + + +const createArtifact = async (config: ConfigService, auth: AuthService, groupId: string|null, data: CreateArtifact): Promise => { + groupId = normalizeGroupId(groupId); + + const baseHref: string = config.artifactsUrl(); + const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts", { groupId }); + const options = await createAuthOptions(auth); + + return httpPostWithReturn(endpoint, data, options); +}; + +const createArtifactVersion = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string, data: CreateVersion): Promise => { + groupId = normalizeGroupId(groupId); + + const baseHref: string = config.artifactsUrl(); + const options = await createAuthOptions(auth); + const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/versions", { groupId, artifactId }); + + return httpPostWithReturn(endpoint, data, options); }; const getArtifactMetaData = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string): Promise => { @@ -174,16 +153,16 @@ const getLatestArtifact = async (config: ConfigService, auth: AuthService, group return getArtifactVersionContent(config, auth, groupId, artifactId, "latest"); }; -const updateArtifactMetaData = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string, metaData: EditableMetaData): Promise => { +const updateArtifactMetaData = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string, metaData: EditableArtifactMetaData): Promise => { groupId = normalizeGroupId(groupId); const baseHref: string = config.artifactsUrl(); const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId", { groupId, artifactId }); const options = await createAuthOptions(auth); - return httpPut(endpoint, metaData, options); + return httpPut(endpoint, metaData, options); }; -const updateArtifactVersionMetaData = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string, version: string, metaData: EditableMetaData): Promise => { +const updateArtifactVersionMetaData = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string, version: string, metaData: EditableVersionMetaData): Promise => { groupId = normalizeGroupId(groupId); const baseHref: string = config.artifactsUrl(); @@ -191,7 +170,7 @@ const updateArtifactVersionMetaData = async (config: ConfigService, auth: AuthSe const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/versions/:versionExpression", { groupId, artifactId, versionExpression }); const options = await createAuthOptions(auth); - return httpPut(endpoint, metaData, options); + return httpPut(endpoint, metaData, options); }; const updateArtifactOwner = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string, newOwner: string): Promise => { @@ -219,19 +198,22 @@ const getArtifactVersionContent = async (config: ConfigService, auth: AuthServic return httpGet(endpoint, options); }; -const getArtifactVersions = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string): Promise => { +const getArtifactVersions = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string, sortBy: VersionSortBy, sortOrder: SortOrder, paging: Paging): Promise => { groupId = normalizeGroupId(groupId); + const start: number = (paging.page - 1) * paging.pageSize; + const end: number = start + paging.pageSize; + const queryParams: any = { + limit: end, + offset: start, + order: sortOrder, + orderby: sortBy + }; console.info("[GroupsService] Getting the list of versions for artifact: ", groupId, artifactId); const baseHref: string = config.artifactsUrl(); - const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/versions", { groupId, artifactId }, { - limit: 500, - offset: 0 - }); + const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/versions", { groupId, artifactId }, queryParams); const options = await createAuthOptions(auth); - return httpGet(endpoint, options, (data) => { - return data.versions; - }); + return httpGet(endpoint, options); }; const getArtifactRules = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string): Promise => { @@ -313,30 +295,54 @@ const deleteArtifact = async (config: ConfigService, auth: AuthService, groupId: return httpDelete(endpoint, options); }; + +const deleteArtifactVersion = async (config: ConfigService, auth: AuthService, groupId: string|null, artifactId: string, version: string): Promise => { + groupId = normalizeGroupId(groupId); + + console.info("[GroupsService] Deleting version: ", groupId, artifactId, version); + const baseHref: string = config.artifactsUrl(); + const endpoint: string = createEndpoint(baseHref, "/groups/:groupId/artifacts/:artifactId/versions/:version", { + groupId, + artifactId, + version + }); + const options = await createAuthOptions(auth); + return httpDelete(endpoint, options); +}; + const normalizeGroupId = (groupId: string|null): string => { return groupId || "default"; }; export interface GroupsService { - createArtifact(data: CreateArtifactData): Promise; - createArtifactVersion(groupId: string|null, artifactId: string, data: CreateVersionData): Promise; - getArtifacts(criteria: GetArtifactsCriteria, paging: Paging): Promise; + createGroup(data: CreateGroup): Promise; + getGroupMetaData(groupId: string): Promise; + getGroupArtifacts(groupId: string, sortBy: ArtifactSortBy, sortOrder: SortOrder, paging: Paging): Promise; + updateGroupMetaData(groupId: string, metaData: EditableGroupMetaData): Promise; + updateGroupOwner(groupId: string, newOwner: string): Promise; + deleteGroup(groupId: string): Promise; + + createArtifact(groupId: string|null, data: CreateArtifact): Promise; getArtifactMetaData(groupId: string|null, artifactId: string): Promise; - getArtifactVersionMetaData(groupId: string|null, artifactId: string, version: string): Promise; getArtifactReferences(globalId: number, refType: ReferenceType): Promise; + getArtifactRules(groupId: string|null, artifactId: string): Promise; + getArtifactVersions(groupId: string|null, artifactId: string, sortBy: VersionSortBy, sortOrder: SortOrder, paging: Paging): Promise; getLatestArtifact(groupId: string|null, artifactId: string): Promise; - updateArtifactMetaData(groupId: string|null, artifactId: string, metaData: EditableMetaData): Promise; - updateArtifactVersionMetaData(groupId: string|null, artifactId: string, version: string, metaData: EditableMetaData): Promise; + updateArtifactMetaData(groupId: string|null, artifactId: string, metaData: EditableArtifactMetaData): Promise; updateArtifactOwner(groupId: string|null, artifactId: string, newOwner: string): Promise; - getArtifactVersionContent(groupId: string|null, artifactId: string, version: string): Promise; - getArtifactVersions(groupId: string|null, artifactId: string): Promise; - getArtifactRules(groupId: string|null, artifactId: string): Promise; - getArtifactRule(groupId: string|null, artifactId: string, type: string): Promise; + deleteArtifact(groupId: string|null, artifactId: string): Promise; + createArtifactRule(groupId: string|null, artifactId: string, type: string, configValue: string): Promise; + getArtifactRule(groupId: string|null, artifactId: string, type: string): Promise; updateArtifactRule(groupId: string|null, artifactId: string, type: string, configValue: string): Promise; deleteArtifactRule(groupId: string|null, artifactId: string, type: string): Promise; - deleteArtifact(groupId: string|null, artifactId: string): Promise; + + createArtifactVersion(groupId: string|null, artifactId: string, data: CreateVersion): Promise; + getArtifactVersionMetaData(groupId: string|null, artifactId: string, version: string): Promise; + getArtifactVersionContent(groupId: string|null, artifactId: string, version: string): Promise; + updateArtifactVersionMetaData(groupId: string|null, artifactId: string, version: string, metaData: EditableVersionMetaData): Promise; + deleteArtifactVersion(groupId: string|null, artifactId: string, version: string): Promise; } @@ -345,59 +351,80 @@ export const useGroupsService: () => GroupsService = (): GroupsService => { const auth = useAuth(); return { - createArtifact(data: CreateArtifactData): Promise { - return createArtifact(config, auth, data); + createGroup(data: CreateGroup): Promise { + return createGroup(config, auth, data); }, - createArtifactVersion(groupId: string|null, artifactId: string, data: CreateVersionData): Promise { - return createArtifactVersion(config, auth, groupId, artifactId, data); + getGroupMetaData(groupId: string): Promise { + return getGroupMetaData(config, auth, groupId); + }, + getGroupArtifacts(groupId: string, sortBy: ArtifactSortBy, sortOrder: SortOrder, paging: Paging): Promise { + return getGroupArtifacts(config, auth, groupId, sortBy, sortOrder, paging); }, - getArtifacts(criteria: GetArtifactsCriteria, paging: Paging): Promise { - return getArtifacts(config, auth, criteria, paging); + updateGroupMetaData(groupId: string, metaData: EditableGroupMetaData): Promise { + return updateGroupMetaData(config, auth, groupId, metaData); + }, + updateGroupOwner(groupId: string, newOwner: string): Promise { + return updateGroupOwner(config, auth, groupId, newOwner); + }, + deleteGroup(groupId: string): Promise { + return deleteGroup(config, auth, groupId); + }, + + createArtifact(groupId: string|null, data: CreateArtifact): Promise { + return createArtifact(config, auth, groupId, data); }, getArtifactMetaData(groupId: string|null, artifactId: string): Promise { return getArtifactMetaData(config, auth, groupId, artifactId); }, - getArtifactVersionMetaData(groupId: string|null, artifactId: string, version: string): Promise { - return getArtifactVersionMetaData(config, auth, groupId, artifactId, version); - }, getArtifactReferences(globalId: number, refType: ReferenceType): Promise { return getArtifactReferences(config, auth, globalId, refType); }, + getArtifactRules(groupId: string|null, artifactId: string): Promise { + return getArtifactRules(config, auth, groupId, artifactId); + }, + getArtifactVersions(groupId: string|null, artifactId: string, sortBy: VersionSortBy, sortOrder: SortOrder, paging: Paging): Promise { + return getArtifactVersions(config, auth, groupId, artifactId, sortBy, sortOrder, paging); + }, getLatestArtifact(groupId: string|null, artifactId: string): Promise { return getLatestArtifact(config, auth, groupId, artifactId); }, - updateArtifactMetaData(groupId: string|null, artifactId: string, metaData: EditableMetaData): Promise { + updateArtifactMetaData(groupId: string|null, artifactId: string, metaData: EditableArtifactMetaData): Promise { return updateArtifactMetaData(config, auth, groupId, artifactId, metaData); }, - updateArtifactVersionMetaData(groupId: string|null, artifactId: string, version: string, metaData: EditableMetaData): Promise { - return updateArtifactVersionMetaData(config, auth, groupId, artifactId, version, metaData); - }, updateArtifactOwner(groupId: string|null, artifactId: string, newOwner: string): Promise { return updateArtifactOwner(config, auth, groupId, artifactId, newOwner); }, - getArtifactVersionContent(groupId: string|null, artifactId: string, version: string): Promise { - return getArtifactVersionContent(config, auth, groupId, artifactId, version); - }, - getArtifactVersions(groupId: string|null, artifactId: string): Promise { - return getArtifactVersions(config, auth, groupId, artifactId); + deleteArtifact(groupId: string|null, artifactId: string): Promise { + return deleteArtifact(config, auth, groupId, artifactId); }, - getArtifactRules(groupId: string|null, artifactId: string): Promise { - return getArtifactRules(config, auth, groupId, artifactId); + + createArtifactRule(groupId: string|null, artifactId: string, type: string, configValue: string): Promise { + return createArtifactRule(config, auth, groupId, artifactId, type, configValue); }, getArtifactRule(groupId: string|null, artifactId: string, type: string): Promise { return getArtifactRule(config, auth, groupId, artifactId, type); }, - createArtifactRule(groupId: string|null, artifactId: string, type: string, configValue: string): Promise { - return createArtifactRule(config, auth, groupId, artifactId, type, configValue); - }, updateArtifactRule(groupId: string|null, artifactId: string, type: string, configValue: string): Promise { return updateArtifactRule(config, auth, groupId, artifactId, type, configValue); }, deleteArtifactRule(groupId: string|null, artifactId: string, type: string): Promise { return deleteArtifactRule(config, auth, groupId, artifactId, type); }, - deleteArtifact(groupId: string|null, artifactId: string): Promise { - return deleteArtifact(config, auth, groupId, artifactId); - } + + createArtifactVersion(groupId: string|null, artifactId: string, data: CreateVersion): Promise { + return createArtifactVersion(config, auth, groupId, artifactId, data); + }, + getArtifactVersionMetaData(groupId: string|null, artifactId: string, version: string): Promise { + return getArtifactVersionMetaData(config, auth, groupId, artifactId, version); + }, + getArtifactVersionContent(groupId: string|null, artifactId: string, version: string): Promise { + return getArtifactVersionContent(config, auth, groupId, artifactId, version); + }, + updateArtifactVersionMetaData(groupId: string|null, artifactId: string, version: string, metaData: EditableVersionMetaData): Promise { + return updateArtifactVersionMetaData(config, auth, groupId, artifactId, version, metaData); + }, + deleteArtifactVersion(groupId: string|null, artifactId: string, version: string): Promise { + return deleteArtifactVersion(config, auth, groupId, artifactId, version); + }, }; }; diff --git a/ui/ui-app/src/services/useSearchService.ts b/ui/ui-app/src/services/useSearchService.ts new file mode 100644 index 0000000000..cd157beef6 --- /dev/null +++ b/ui/ui-app/src/services/useSearchService.ts @@ -0,0 +1,78 @@ +import { ConfigService, useConfigService } from "@services/useConfigService.ts"; +import { createAuthOptions, createEndpoint, httpGet } from "@utils/rest.utils.ts"; +import { AuthService, useAuth } from "@apicurio/common-ui-components"; +import { ArtifactSearchResults } from "@models/artifactSearchResults.model.ts"; +import { Paging } from "@models/paging.model.ts"; +import { GroupSearchResults } from "@models/groupSearchResults.model.ts"; +import { ArtifactSortBy } from "@models/artifactSortBy.model.ts"; +import { GroupSortBy } from "@models/groupSortBy.model.ts"; +import { SortOrder } from "@models/sortOrder.model.ts"; + +export enum FilterBy { + name = "name", description = "description", labels = "labels", groupId = "groupId", artifactId = "artifactId", + globalId = "globalId", contentId = "contentId" +} + +export interface SearchFilter { + by: FilterBy; + value: string; +} + +const searchArtifacts = async (config: ConfigService, auth: AuthService, filters: SearchFilter[], sortBy: ArtifactSortBy, sortOrder: SortOrder, paging: Paging): Promise => { + console.debug("[SearchService] Searching artifacts: ", filters, sortBy, sortOrder, paging); + const start: number = (paging.page - 1) * paging.pageSize; + const end: number = start + paging.pageSize; + const queryParams: any = { + limit: end, + offset: start, + order: sortOrder, + orderby: sortBy + }; + filters?.forEach(filter => { + queryParams[filter.by] = filter.value; + }); + const baseHref: string = config.artifactsUrl(); + const endpoint: string = createEndpoint(baseHref, "/search/artifacts", {}, queryParams); + const options = await createAuthOptions(auth); + return httpGet(endpoint, options); +}; + +const searchGroups = async (config: ConfigService, auth: AuthService, filters: SearchFilter[], sortBy: GroupSortBy, sortOrder: SortOrder, paging: Paging): Promise => { + console.debug("[SearchService] Searching groups: ", filters, paging); + const start: number = (paging.page - 1) * paging.pageSize; + const end: number = start + paging.pageSize; + const queryParams: any = { + limit: end, + offset: start, + order: sortOrder, + orderby: sortBy + }; + filters.forEach(filter => { + queryParams[filter.by] = filter.value; + }); + const baseHref: string = config.artifactsUrl(); + const endpoint: string = createEndpoint(baseHref, "/search/groups", {}, queryParams); + const options = await createAuthOptions(auth); + return httpGet(endpoint, options); +}; + + +export interface SearchService { + searchArtifacts(filters: SearchFilter[], sortBy: ArtifactSortBy, sortOrder: SortOrder, paging: Paging): Promise; + searchGroups(filters: SearchFilter[], sortBy: GroupSortBy, sortOrder: SortOrder, paging: Paging): Promise; +} + + +export const useSearchService: () => SearchService = (): SearchService => { + const config: ConfigService = useConfigService(); + const auth = useAuth(); + + return { + searchArtifacts(filters: SearchFilter[], sortBy: ArtifactSortBy, sortOrder: SortOrder, paging: Paging): Promise { + return searchArtifacts(config, auth, filters, sortBy, sortOrder, paging); + }, + searchGroups(filters: SearchFilter[], sortBy: GroupSortBy, sortOrder: SortOrder, paging: Paging): Promise { + return searchGroups(config, auth, filters, sortBy, sortOrder, paging); + }, + }; +}; diff --git a/ui/ui-app/src/utils/content.utils.ts b/ui/ui-app/src/utils/content.utils.ts index 4446e29290..397ca97ae7 100644 --- a/ui/ui-app/src/utils/content.utils.ts +++ b/ui/ui-app/src/utils/content.utils.ts @@ -99,7 +99,7 @@ export function contentToString(content: any): string { } -export function contentType(type: string, content: string): string { +export function detectContentType(type: string, content: string): string { switch (type) { case "PROTOBUF": return ContentTypes.APPLICATION_PROTOBUF; @@ -117,6 +117,6 @@ export function contentType(type: string, content: string): string { } else if (isYaml(content)) { return ContentTypes.APPLICATION_YAML; } else { - return "application/octet-stream"; + return ContentTypes.APPLICATION_OCTET_STREAM; } } diff --git a/ui/ui-app/src/utils/index.ts b/ui/ui-app/src/utils/index.ts index 420cd2308d..595e68356d 100644 --- a/ui/ui-app/src/utils/index.ts +++ b/ui/ui-app/src/utils/index.ts @@ -1,3 +1,4 @@ export * from "./content.utils"; export * from "./object.utils"; export * from "./rest.utils"; +export * from "./string.utils"; diff --git a/ui/ui-app/src/utils/string.utils.ts b/ui/ui-app/src/utils/string.utils.ts new file mode 100644 index 0000000000..a89f993855 --- /dev/null +++ b/ui/ui-app/src/utils/string.utils.ts @@ -0,0 +1,25 @@ +/* + cyrb53 (c) 2018 bryc (github.com/bryc) + License: Public domain (or MIT if needed). Attribution appreciated. + A fast and simple 53-bit string hash function with decent collision resistance. + Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity. + + Reference: https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js#L7-L19 +*/ +export const shash = (str: string, seed: number = 0): number => { + let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; + for(let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return 4294967296 * (2097151 & h2) + (h1 >>> 0); +}; + +export const isStringEmptyOrUndefined = (str: string | null | undefined): boolean => { + return str === undefined || str === null || str.trim().length === 0; +};