11import "./ApplicationDetail.scss" ;
22import React , { useEffect , useState } from "react" ;
3- import { connectServiceProviderToIdentityProvider , publicServiceProviderByDetail } from "../api/index.js" ;
3+ import {
4+ connectServiceProviderToIdentityProvider ,
5+ getPolicyByServiceProviderEntityId ,
6+ inviteRoles ,
7+ publicServiceProviderByDetail
8+ } from "../api/index.js" ;
49import I18n from "../locale/I18n.js" ;
510import ExternalLinkIcon from "../icons/external-link.svg" ;
611import NotAllowedIcon from "../icons/not-allowed.svg" ;
712import { useNavigate , useParams } from "react-router-dom" ;
813import {
14+ Alert ,
15+ AlertType ,
916 Button ,
1017 ButtonIconPlacement ,
1118 ButtonType ,
@@ -21,6 +28,7 @@ import ArrowLeftIcon from "@surfnet/sds/icons/functional-icons/arrow-left-2.svg"
2128import {
2229 APPLICATION_LINKS ,
2330 CHANGE_REQUEST_TYPE ,
31+ isAccessRoleReady ,
2432 providerDescription ,
2533 providerName ,
2634 providerOrganizationName
@@ -34,6 +42,7 @@ import InputField from "../components/InputField.jsx";
3442import { mainMenuItems } from "../utils/MenuItems.js" ;
3543import { TabHeader } from "../components/TabHeader.jsx" ;
3644import { InfoBlock } from "../components/InfoBlock.jsx" ;
45+ import DOMPurify from "dompurify" ;
3746
3847const confirmationModalOptions = {
3948 makeConnection : "makeConnection" ,
@@ -43,8 +52,6 @@ const confirmationModalOptions = {
4352 requestDisconnectConnection : "requestDisconnectConnection" ,
4453}
4554
46- const tabNames = [ "access" , "information" ]
47-
4855const ApplicationDetail = ( { anonymous, refreshUser} ) => {
4956
5057 const { arp, privacy, user, config, setFlash} = useAppStore ( useShallow ( state => ( {
@@ -58,9 +65,12 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
5865 const navigate = useNavigate ( ) ;
5966 const { manageType, manageId, tab = "access" } = useParams ( ) ;
6067
68+ const [ tabNames , setTabNames ] = useState ( [ "access" , "information" ] ) ;
6169 const [ currentTab , setCurrentTab ] = useState ( tab ) ;
6270 const [ loading , setLoading ] = useState ( true ) ;
63- const [ serviceProvider , setServiceProvider ] = useState ( [ ] ) ;
71+ const [ serviceProvider , setServiceProvider ] = useState ( { } ) ;
72+ const [ accessRoles , setAccessRoles ] = useState ( { } ) ;
73+ const [ policies , setPolicies ] = useState ( { } ) ;
6474 const [ showAttributes , setShowAttributes ] = useState ( false ) ;
6575 const [ showPrivacy , setShowPrivacy ] = useState ( false ) ;
6676 const [ metaData , setMetaData ] = useState ( { } ) ;
@@ -80,19 +90,23 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
8090 setServiceProvider ( res ) ;
8191 const newMetaData = res . data . metaDataFields ;
8292 setMetaData ( newMetaData ) ;
93+ if ( anonymous ) {
94+ setLoading ( false ) ;
95+ return ;
96+ }
8397 //See if this application is already connected
8498 const allowedEntities = user . identityProvider . data . allowedEntities . map ( entity => entity . name ) ;
8599 let isAccessible = allowedEntities . includes ( res . data . entityid ) ;
100+ let isReadOnly = true ;
86101 if ( isAccessible ) {
87- setReadOnly ( false ) ;
102+ isReadOnly = false ;
88103 } else {
89104 //Check if there is an outstanding change request
90105 isAccessible = user . changeRequests
91106 . some ( changeRequest => changeRequest . requestType === CHANGE_REQUEST_TYPE . LINK_REQUEST &&
92107 changeRequest . pathUpdateType === "ADDITION" &&
93108 changeRequest . pathUpdates . allowedEntities . name === res . data . entityid
94109 ) ;
95- setReadOnly ( true ) ;
96110 }
97111 setAccessible ( isAccessible ) ;
98112 useAppStore . setState ( {
@@ -112,8 +126,32 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
112126 setConnectWithoutInteraction ( connectOption !== "connect_with_interaction" || sameInstitution ) ;
113127 const idpId = user . identityProvider . id
114128 const orgMembership = user . organizationMemberships . find ( orgMembership => orgMembership . organization . manageIdentifier === idpId ) ;
115- setIsAdminUser ( user . superUser || ( ! isEmpty ( orgMembership ) && orgMembership . authority === authorities . ADMIN ) ) ;
116- setLoading ( false ) ;
129+ const adminUser = user . superUser || ( ! isEmpty ( orgMembership ) && orgMembership . authority === authorities . ADMIN ) ;
130+ setIsAdminUser ( adminUser ) ;
131+ setReadOnly ( isReadOnly ) ;
132+ if ( isAccessible ) {
133+ if ( adminUser ) {
134+ if ( isReadOnly ) {
135+ setLoading ( false ) ;
136+ } else {
137+ Promise . all ( [
138+ inviteRoles ( user . organizationGUID , res . id ) ,
139+ getPolicyByServiceProviderEntityId ( res . data . entityid )
140+ ] ) . then ( res => {
141+ setAccessRoles ( res [ 0 ] ) ;
142+ setPolicies ( res [ 1 ] ) ;
143+ setLoading ( false ) ;
144+ } )
145+ }
146+ } else {
147+ setTabNames ( [ "information" ] ) ;
148+ setCurrentTab ( "information" ) ;
149+ setLoading ( false ) ;
150+ }
151+ } else {
152+ setLoading ( false ) ;
153+ }
154+
117155 } )
118156 . catch ( ( ) => {
119157 navigate ( "/404" ) ;
@@ -201,7 +239,7 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
201239 ) ;
202240
203241 }
204- return "TODO"
242+ return null ;
205243 }
206244
207245 const cancelConfirmation = ( ) => {
@@ -210,6 +248,23 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
210248 setAccessChoice ( "ALL" )
211249 }
212250
251+ const cancelConnectionRequest = ( withConfirmation , e ) => {
252+ stopEvent ( e ) ;
253+ if ( withConfirmation ) {
254+ setConfirmation ( {
255+ open : true ,
256+ cancel : ( ) => cancelConfirmation ( ) ,
257+ action : ( ) => cancelConnectionRequest ( false ) ,
258+ title : I18n . t ( "appAccess.cancelRequestTitle" ) ,
259+ okButton : I18n . t ( "forms.sure" ) ,
260+ question : I18n . t ( "appAccess.cancelRequestQuestion" ) ,
261+ } ) ;
262+ } else {
263+ cancelConfirmation ( ) ;
264+ alert ( "cancel request" )
265+ }
266+ }
267+
213268 const doRequestConnection = ( withConfirmation , modalOption ) => {
214269 if ( withConfirmation ) {
215270 let newModalOption ;
@@ -280,52 +335,94 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
280335
281336 const renderAccessApp = ( ) => {
282337 return (
283- < div className = "app-access" >
284- < div className = "app-access-central" >
285- < h2 > { I18n . t ( "appAccess.title" ) } </ h2 >
286- < InfoBlock className = "no-gap" >
287- < div className = "grouped" >
288- < div >
289- < h3 > { I18n . t ( "appAccess.users" , { name : providerOrganizationName ( I18n . locale , serviceProvider ) } ) } </ h3 >
290- < p > { I18n . t ( "appAccess.config" ) } </ p >
338+ < >
339+ { readOnly && < Alert alertType = { AlertType . Warning }
340+ asChild = { true }
341+ children = { < a href = "/" onClick = { e => cancelConnectionRequest ( true , e ) } >
342+ { I18n . t ( "appAccess.cancelRequest" ) } </ a > }
343+ message = { I18n . t ( "appAccess.requestedAccessNotification" ) } />
344+ }
345+ < div className = { `app-access ${ readOnly ? "read-only" : "" } ` } onClick = { e => readOnly && stopEvent ( e ) } >
346+ < div className = "app-access-central" >
347+ < h2 > { I18n . t ( "appAccess.title" ) } </ h2 >
348+ < InfoBlock className = "no-gap" >
349+ < div className = "grouped" >
350+ < div >
351+ < h3 > { I18n . t ( "appAccess.users" , { name : providerOrganizationName ( I18n . locale , serviceProvider ) } ) } </ h3 >
352+ < p > { I18n . t ( "appAccess.config" ) } </ p >
353+ </ div >
354+ < Button type = { ButtonType . Primary }
355+ onClick = { ( ) => alert ( "ToDo" ) }
356+ txt = { I18n . t ( "forms.edit" ) } />
291357 </ div >
292- < Button type = { ButtonType . Primary }
293- onClick = { ( ) => alert ( "ToDo" ) }
294- txt = { I18n . t ( "forms.edit" ) } />
295- </ div >
296- < p > { I18n . t ( "appAccess.accessFor" ) } </ p >
297- < div className = "access-card" >
298- < h4 > { I18n . t ( "appAccess.everyBody" , { name : providerOrganizationName ( I18n . locale , serviceProvider ) } ) } </ h4 >
299- { renderLogo ( user . identityProvider . data . metaDataFields ) }
300- </ div >
301- </ InfoBlock >
302- < InfoBlock className = "no-gap" >
303- < div className = "grouped" >
304- < div >
305- < h3 > { I18n . t ( "appAccess.outSideUsers" ) } </ h3 >
306- < p > { I18n . t ( "appAccess.roleBasedAccess" ) } </ p >
358+ < p > { I18n . t ( "appAccess.accessFor" ) } </ p >
359+ < div className = "access-card large" >
360+ < h4 > { I18n . t ( "appAccess.everyBody" , { name : providerOrganizationName ( I18n . locale , serviceProvider ) } ) } </ h4 >
361+ { renderLogo ( user . identityProvider . data . metaDataFields ) }
307362 </ div >
308- < Button type = { ButtonType . Primary }
309- onClick = { ( ) => window . open ( config . invite , "_blank" ) . focus ( ) }
310- icon = { < ExternalLinkIcon /> }
311- txt = { I18n . t ( "appAccess.roleManagement" ) } />
312- </ div >
313- < div className = "access-card grey" >
314- < p > { I18n . t ( "appAccess.noRoles" ) } </ p >
315- </ div >
316- </ InfoBlock >
317- </ div >
318- < div className = "app-access-decentral" >
319- < h2 > { I18n . t ( "appAccess.decentralAccess" ) } </ h2 >
320- < InfoBlock className = "no-gap grey row" >
321- < div className = "not-allowed-container" >
322- < NotAllowedIcon />
323- < p
324- dangerouslySetInnerHTML = { { __html : I18n . t ( "appAccess.noDecentralAccess" ) } } />
325- </ div >
326- </ InfoBlock >
363+ { ! isEmpty ( policies ) && < >
364+ { policies . map ( ( policy , index ) =>
365+ < div key = { index } className = "access-card large" >
366+ { policy . data . name }
367+
368+ </ div > ) }
369+
370+ </ > }
371+ </ InfoBlock >
372+ < InfoBlock className = "no-gap" >
373+ < div className = "grouped" >
374+ < div >
375+ < h3 > { I18n . t ( "appAccess.outSideUsers" ) } </ h3 >
376+ < p > { I18n . t ( "appAccess.roleBasedAccess" ) } </ p >
377+ </ div >
378+ < Button type = { ButtonType . Primary }
379+ onClick = { ( ) => window . open ( `${ config . invite } /applications/${ serviceProvider . id } ` ,
380+ "_blank" ) . focus ( ) }
381+ icon = { < ExternalLinkIcon /> }
382+ txt = { I18n . t ( "appAccess.roleManagement" ) } />
383+ </ div >
384+ { isEmpty ( accessRoles ) &&
385+ < div className = "access-card grey" >
386+ < p > { I18n . t ( "appAccess.noRoles" ) } </ p >
387+ </ div > }
388+ { ! isEmpty ( accessRoles ) &&
389+ < >
390+ < p > { I18n . t ( "appAccess.accessFor" ) } </ p >
391+ { accessRoles . map ( ( role , index ) =>
392+ < div key = { index } className = "access-card column large" >
393+ < div >
394+ < p dangerouslySetInnerHTML = { {
395+ __html : DOMPurify . sanitize (
396+ I18n . t ( "appAccess.roleUsers" , { count : role . userRoleCount } ) )
397+ } } />
398+ < p > < strong > { role . name } </ strong > </ p >
399+ </ div >
400+ < div className = { `chip ${ role . eduIDOnly ? "blue" : "" } ` } >
401+ { I18n . t ( `appAccess.${ role . eduIDOnly ? "eduIDOnly" : "everyIdp" } ` ) }
402+ </ div >
403+
404+
405+ </ div > ) }
406+ </ >
407+ }
408+ < em className = "role-ready" dangerouslySetInnerHTML = { {
409+ __html : DOMPurify . sanitize (
410+ I18n . t ( `appAccess.${ isAccessRoleReady ( serviceProvider ) ? "roleReady" : "notRoleReady" } ` ) )
411+ } } />
412+ </ InfoBlock >
413+ </ div >
414+ < div className = "app-access-decentral" >
415+ < h2 > { I18n . t ( "appAccess.decentralAccess" ) } </ h2 >
416+ < InfoBlock className = "no-gap grey row" >
417+ < div className = "not-allowed-container" >
418+ < NotAllowedIcon />
419+ < p
420+ dangerouslySetInnerHTML = { { __html : I18n . t ( "appAccess.noDecentralAccess" ) } } />
421+ </ div >
422+ </ InfoBlock >
423+ </ div >
327424 </ div >
328- </ div >
425+ </ >
329426 ) ;
330427 }
331428
@@ -451,7 +548,7 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
451548
452549 const renderDetailsApp = ( ) => {
453550 return (
454- < div className = " details" >
551+ < div className = { ` details ${ anonymous ? "anonymous" : "" } ` } >
455552 < div className = "left" >
456553 < p > { providerDescription ( I18n . locale , serviceProvider ) } </ p >
457554 { renderAppAttributes ( ) }
@@ -550,7 +647,7 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
550647 }
551648
552649 return (
553- < div className = " application-detail-container" >
650+ < div className = { ` application-detail-container` } >
554651 { open && < ConfirmationDialog confirm = { action }
555652 cancel = { cancel }
556653 confirmationTxt = { okButton }
0 commit comments