diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java index f7cfb019a..e57870cb9 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java @@ -83,7 +83,7 @@ public ResponseEntity deleteOne(@PathVariable String resourceId) throws Forbi @GetMapping("/EntityDescriptors") @Transactional public ResponseEntity getAll() throws ForbiddenException { - return ResponseEntity.ok(entityDescriptorService.getAllRepresentationsBasedOnUserAccess()); + return ResponseEntity.ok(entityDescriptorService.getAllEntityDescriptorProjectionsBasedOnUserAccess()); } @GetMapping("/EntityDescriptor/{resourceId}/Versions") @@ -145,13 +145,20 @@ public void initRestTemplate() { @PutMapping("/EntityDescriptor/{resourceId}") @Transactional public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) - throws ForbiddenException, ConcurrentModificationException, PersistentEntityNotFound, - InvalidPatternMatchException { + throws ForbiddenException, ConcurrentModificationException, PersistentEntityNotFound, InvalidPatternMatchException { edRepresentation.setId(resourceId); // This should be the same already, but just to be safe... EntityDescriptorRepresentation result = entityDescriptorService.update(edRepresentation); return ResponseEntity.ok().body(result); } + @PutMapping("/EntityDescriptor/{resourceId}/changeGroup/{groupId}") + @Transactional + public ResponseEntity updateGroupForEntityDescriptor(@PathVariable String resourceId, @PathVariable String groupId) + throws ForbiddenException, ConcurrentModificationException, PersistentEntityNotFound, InvalidPatternMatchException { + EntityDescriptorRepresentation result = entityDescriptorService.updateGroupForEntityDescriptor(resourceId, groupId); + return ResponseEntity.ok().body(result); + } + @PostMapping(value = "/EntityDescriptor", consumes = "application/xml") @Transactional public ResponseEntity upload(@RequestBody byte[] entityDescriptorXml, @RequestParam String spName) throws Exception { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorProjection.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorProjection.java new file mode 100644 index 000000000..57cf02ab9 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorProjection.java @@ -0,0 +1,19 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import java.time.LocalDateTime; + +public interface EntityDescriptorProjection { + default String getId() { + return getResourceId(); + } + String getEntityID(); + default String getEntityId() { + return getEntityID(); + } + String getResourceId(); + String getServiceProviderName(); + String getCreatedBy(); + LocalDateTime getCreatedDate(); + boolean getServiceEnabled(); + String getIdOfOwner(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java index 5e28e7d75..bb2b275d6 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java @@ -12,6 +12,9 @@ * Repository to manage {@link EntityDescriptor} instances. */ public interface EntityDescriptorRepository extends JpaRepository { + List findAllBy(); + + List findAllByIdOfOwner(String ownerId); EntityDescriptor findByEntityID(String entityId); @@ -34,4 +37,4 @@ public interface EntityDescriptorRepository extends JpaRepository findAllByIdOfOwnerIsNull(); -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/ProjectionIdAndName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/ProjectionIdAndName.java index 6731aea86..fca5ae8cd 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/ProjectionIdAndName.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/ProjectionIdAndName.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.repository; -public interface ProjectionIdAndName{ +public interface ProjectionIdAndName { String getResourceId(); + String getName(); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java index 6d66732b0..fa8fc62f8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java @@ -3,10 +3,11 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidPatternMatchException; import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound; +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorProjection; import java.util.ConcurrentModificationException; import java.util.List; @@ -67,9 +68,9 @@ EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepres Iterable getAllDisabledAndNotOwnedByAdmin() throws ForbiddenException; /** - * @return a list of EntityDescriptorRepresentations that a user has the rights to access + * @return a list of EntityDescriptorProjections that a user has the rights to access */ - List getAllRepresentationsBasedOnUserAccess() throws ForbiddenException; + List getAllEntityDescriptorProjectionsBasedOnUserAccess() throws ForbiddenException; /** * Given a list of attributes, generate an AttributeReleaseList @@ -119,4 +120,6 @@ EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resour EntityDescriptorRepresentation createNewEntityDescriptorFromXMLOrigin(EntityDescriptor ed); boolean entityExists(String entityID); + + EntityDescriptorRepresentation updateGroupForEntityDescriptor(String resourceId, String groupId); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index 6269020e8..291f659f8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -21,6 +21,7 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidPatternMatchException; import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorProjection; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.model.Owner; @@ -118,6 +119,14 @@ public boolean entityExists(String entityID) { return entityDescriptorRepository.findByEntityID(entityID) != null ; } + @Override + public EntityDescriptorRepresentation updateGroupForEntityDescriptor(String resourceId, String groupId) { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + ed.setIdOfOwner(groupId); + EntityDescriptor savedEntity = entityDescriptorRepository.save(ed); + return createRepresentationFromDescriptor(savedEntity); + } + @Override public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { @@ -373,16 +382,16 @@ public Iterable getAllDisabledAndNotOwnedByAdmin } @Override - public List getAllRepresentationsBasedOnUserAccess() throws ForbiddenException { + public List getAllEntityDescriptorProjectionsBasedOnUserAccess() throws ForbiddenException { switch (userService.getCurrentUserAccess()) { case ADMIN: - return entityDescriptorRepository.findAllStreamByCustomQuery().map(ed -> createRepresentationFromDescriptor(ed)) - .collect(Collectors.toList()); + List o = entityDescriptorRepository.findAllBy(); + return o; case GROUP: User user = userService.getCurrentUser(); Group group = user.getGroup(); - return entityDescriptorRepository.findAllStreamByIdOfOwner(group.getOwnerId()) - .map(ed -> createRepresentationFromDescriptor(ed)).collect(Collectors.toList()); + List ed = entityDescriptorRepository.findAllByIdOfOwner(group.getOwnerId()); + return ed; default: throw new ForbiddenException(); } diff --git a/ui/src/app/core/components/Spinner.js b/ui/src/app/core/components/Spinner.js index 880f53d2a..c137bb9ac 100644 --- a/ui/src/app/core/components/Spinner.js +++ b/ui/src/app/core/components/Spinner.js @@ -2,8 +2,8 @@ import React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; -export function Spinner ({ size, className }) { - return () +export function Spinner (props) { + return () } export default Spinner; \ No newline at end of file diff --git a/ui/src/app/dashboard/view/SourcesTab.js b/ui/src/app/dashboard/view/SourcesTab.js index 40ff4dd4f..b0d1352d3 100644 --- a/ui/src/app/dashboard/view/SourcesTab.js +++ b/ui/src/app/dashboard/view/SourcesTab.js @@ -37,7 +37,7 @@ export function SourcesTab () { React.useEffect(() => { loadSources() }, []); async function changeSourceGroup(source, group) { - await updater.put(`/${source.id}`, { + await updater.put(`/${source.id}/changeGroup/${group}`, { ...source, idOfOwner: group }); diff --git a/ui/src/app/metadata/copy/CopySource.js b/ui/src/app/metadata/copy/CopySource.js index f3ae6b142..d7d0be54a 100644 --- a/ui/src/app/metadata/copy/CopySource.js +++ b/ui/src/app/metadata/copy/CopySource.js @@ -34,7 +34,7 @@ export function CopySource({ copy, onNext }) { setSelected([]); }; - const { register, handleSubmit, control, formState, setValue, getValues } = useForm({ + const { register, handleSubmit, control, formState, setValue, getValues, watch } = useForm({ mode: 'onChange', reValidateMode: 'onBlur', defaultValues: { @@ -47,6 +47,8 @@ export function CopySource({ copy, onNext }) { shouldUnregister: false, }); + const target = watch('target'); + const { errors, isValid } = formState; React.useEffect(() => { @@ -92,25 +94,28 @@ export function CopySource({ copy, onNext }) {
- - - Select the Entity ID to copy - - - + + + + Select the Entity ID to copy + + + Entity ID to copy is Required - + + Metadata Source Name (Dashboard Display Only) - + + + aria-describedby="serviceProviderName-help" disabled={!target} /> Service Resolver Name is required @@ -118,12 +123,15 @@ export function CopySource({ copy, onNext }) { + New Entity ID - + + !(sourceIds.indexOf(v) > -1) diff --git a/ui/src/app/metadata/copy/EntityTypeahead.js b/ui/src/app/metadata/copy/EntityTypeahead.js index ed8b9a14e..716089039 100644 --- a/ui/src/app/metadata/copy/EntityTypeahead.js +++ b/ui/src/app/metadata/copy/EntityTypeahead.js @@ -1,10 +1,16 @@ +import { faSpinner } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React from 'react'; import { Typeahead } from 'react-bootstrap-typeahead'; import { useController } from 'react-hook-form'; -import { useMetadataSources } from '../hooks/api'; +import { useMetadataEntity, useMetadataSources } from '../hooks/api'; +import Form from 'react-bootstrap/Form'; -export function EntityTypeahead ({id, control, name}) { +export function EntityTypeahead ({id, control, name, children}) { const { data = [] } = useMetadataSources({}, []); + + const { get, response, loading } = useMetadataEntity(); + const entities = React.useMemo(() => data.map(d => d.entityId), [data]); const { @@ -18,14 +24,30 @@ export function EntityTypeahead ({id, control, name}) { defaultValue: "", }); + const loadSelected = async (selected) => { + const toCopy = data.find(e => e.entityId === selected); + + const source = await get(`/${toCopy.id}`); + if (response.ok) { + onChange(source); + } + // + } + return ( - onChange(selected ? data.find(e => e.entityId === selected[0]) : '')} - defaultInputValue={value ? value.entityId : ''} - options={entities} - required={true} - id={id} - /> + + + {children} + {loading && } + + selected && selected.length > 0 ? loadSelected(selected[0]) : null} + defaultInputValue={value ? value.entityId : ''} + options={entities} + required={true} + id={id} + /> + ) } \ No newline at end of file diff --git a/ui/src/app/metadata/hoc/MetadataSelector.js b/ui/src/app/metadata/hoc/MetadataSelector.js index b81db74b2..54a49d4df 100644 --- a/ui/src/app/metadata/hoc/MetadataSelector.js +++ b/ui/src/app/metadata/hoc/MetadataSelector.js @@ -1,5 +1,6 @@ import React from 'react'; import { useLocation, useParams } from 'react-router'; +import Spinner from '../../core/components/Spinner'; import { useMetadataEntity } from '../hooks/api'; export const MetadataTypeContext = React.createContext(); @@ -10,6 +11,7 @@ export const MetadataLoaderContext = React.createContext(); export function MetadataSelector({ children, ...props }) { let { type, id } = useParams(); + const [loading, setLoading] = React.useState(false); const location = useLocation(); if (!type) { @@ -30,25 +32,35 @@ export function MetadataSelector({ children, ...props }) { const source = await get(`/${id}`); if (response.ok) { setMetadata(source); + setLoading(false); } } function reload() { + setLoading(true); loadMetadata(id); + } React.useEffect(() => reload(), [id]); return ( - - {type && - - {metadata && metadata.version && - {children(metadata, reload)} + + {loading &&
+ +
} + + {type && + + {metadata && metadata.version && + + {children(metadata, reload)} + + } + } -
- } -
+ + ); }