Skip to content

Commit

Permalink
Merge pull request #1050 from openedx/asheehan-edx/ENT-7579
Browse files Browse the repository at this point in the history
feat: implementing sso orchestrator existing configs page
  • Loading branch information
alex-sheehan-edx authored Oct 16, 2023
2 parents 9700727 + b71bdff commit 450bc0e
Show file tree
Hide file tree
Showing 14 changed files with 1,386 additions and 32 deletions.
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ USE_API_CACHE='true'
SUBSCRIPTION_LPR='true'
PLOTLY_SERVER_URL='http://localhost:8050'
AUTH0_SELF_SERVICE_INTEGRATION='true'
FEATURE_SSO_SETTINGS_TAB='true'
44 changes: 44 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@edx/frontend-enterprise-utils": "3.2.0",
"@edx/frontend-platform": "4.0.1",
"@edx/paragon": "20.39.2",
"@tanstack/react-query": "^4.35.7",
"algoliasearch": "4.8.3",
"axios-mock-adapter": "1.19.0",
"classnames": "2.2.6",
Expand Down
181 changes: 181 additions & 0 deletions src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import _ from 'lodash';
import {
CardGrid,
Skeleton,
useToggle,
} from '@edx/paragon';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import LmsApiService from '../../../data/services/LmsApiService';
import NewSSOConfigAlerts from './NewSSOConfigAlerts';
import NewSSOConfigCard from './NewSSOConfigCard';

const FRESH_CONFIG_POLLING_INTERVAL = 30000;
const UPDATED_CONFIG_POLLING_INTERVAL = 2000;

const NewExistingSSOConfigs = ({
configs, refreshBool, setRefreshBool, enterpriseId,
}) => {
const [inactiveConfigs, setInactiveConfigs] = useState([]);
const [activeConfigs, setActiveConfigs] = useState([]);
const [inProgressConfigs, setInProgressConfigs] = useState([]);
const [untestedConfigs, setUntestedConfigs] = useState([]);
const [liveConfigs, setLiveConfigs] = useState([]);
const [notConfiguredConfigs, setNotConfiguredConfigs] = useState([]);
const [queryForTestedConfigs, setQueryForTestedConfigs] = useState(false);
const [queryForConfiguredConfigs, setQueryForConfiguredConfigs] = useState(false);
const [intervalMs, setIntervalMs] = React.useState(FRESH_CONFIG_POLLING_INTERVAL);
const [loading, setLoading] = useState(false);
const [showAlerts, openAlerts, closeAlerts] = useToggle(false);

const queryClient = useQueryClient();

const renderCards = (gridTitle, configList) => {
if (configList.length > 0) {
return (
<div>
<h3 className="mb-4.5">{gridTitle}</h3>
<CardGrid
key={gridTitle}
className="mb-2 mr-3"
columnSizes={{
xs: 9,
s: 9,
m: 9,
l: 9,
xl: 9,
}}
>
{configList.map((config) => (
<NewSSOConfigCard
key={config.uuid}
config={config}
setLoading={setLoading}
setRefreshBool={setRefreshBool}
refreshBool={refreshBool}
/>
))}
</CardGrid>
</div>
);
}
return null;
};

useEffect(() => {
const [active, inactive] = _.partition(configs, config => config.active);
const inProgress = configs.filter(
config => (config.submitted_at && !config.configured_at) || (config.configured_at < config.submitted_at),
);
const untested = configs.filter(config => !config.validated_at);
const live = configs.filter(
config => (config.validated_at && config.active && config.validated_at > config.configured_at),
);
const notConfigured = configs.filter(config => !config.configured_at);

if (live.length >= 1) {
setLiveConfigs(live);
openAlerts();
}

setUntestedConfigs(untested);
if (untested.length >= 1) {
setQueryForTestedConfigs(true);
openAlerts();
}
setInProgressConfigs(inProgress);
if (inProgress.length >= 1) {
const beenConfigured = inProgress.filter(config => config.configured_at);
if (beenConfigured.length >= 1) {
setIntervalMs(UPDATED_CONFIG_POLLING_INTERVAL);
}
setQueryForConfiguredConfigs(true);
openAlerts();
}

if (notConfigured.length >= 1) {
setNotConfiguredConfigs(notConfigured);
}

setActiveConfigs(active);
setInactiveConfigs(inactive);
setLoading(false);
}, [configs, refreshBool, openAlerts]);

useQuery({
queryKey: ['ssoOrchestratorConfigPoll'],
queryFn: async () => {
const res = await LmsApiService.listEnterpriseSsoOrchestrationRecords(enterpriseId);
const inProgress = res.data.filter(
config => (config.submitted_at && !config.configured_at) || (config.configured_at < config.submitted_at),
);
const untested = res.data.filter(config => !config.validated_at || config.validated_at < config.configured_at);

if (queryForConfiguredConfigs) {
if (inProgress.length === 0) {
setRefreshBool(!refreshBool);
setQueryForConfiguredConfigs(false);
}
}

if (queryForTestedConfigs) {
if (untested.length === 0) {
setRefreshBool(!refreshBool);
setQueryForTestedConfigs(false);
}
}

if (inProgress.length === 0 && untested.length === 0) {
queryClient.invalidateQueries({ queryKey: ['ssoOrchestratorConfigPoll'] });
}

return res.data;
},
refetchInterval: intervalMs,
enabled: queryForTestedConfigs || queryForConfiguredConfigs,
refetchOnWindowFocus: true,
});

return (
<>
{!loading && (
<>
{showAlerts && (
<NewSSOConfigAlerts
liveConfigs={liveConfigs}
inProgressConfigs={inProgressConfigs}
untestedConfigs={untestedConfigs}
notConfigured={notConfiguredConfigs}
closeAlerts={closeAlerts}
/>
)}
{renderCards('Active', activeConfigs)}
{renderCards('Inactive', inactiveConfigs)}
</>
)}
{loading && (
<div data-testid="sso-self-service-skeleton">
<Skeleton />
<Skeleton />
<Skeleton />
<Skeleton />
</div>
)}
</>
);
};

NewExistingSSOConfigs.propTypes = {
configs: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
refreshBool: PropTypes.bool.isRequired,
setRefreshBool: PropTypes.func.isRequired,
enterpriseId: PropTypes.string.isRequired,
};

const mapStateToProps = state => ({
enterpriseId: state.portalConfiguration.enterpriseId,
});

export default connect(mapStateToProps)(NewExistingSSOConfigs);
90 changes: 90 additions & 0 deletions src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
CheckCircle, Warning,
} from '@edx/paragon/icons';
import { Alert } from '@edx/paragon';

const NewSSOConfigAlerts = ({
inProgressConfigs,
untestedConfigs,
liveConfigs,
notConfigured,
contactEmail,
closeAlerts,
}) => (
<>
{inProgressConfigs.length >= 1 && (
<Alert
variant="warning"
icon={Warning}
className="ml-0 w-75"
dismissible
onClose={closeAlerts}
>
<Alert.Heading>Your SSO Integration is in progress</Alert.Heading>
<p>
edX is configuring your SSO. This step takes approximately{' '}
{notConfigured.length > 0 ? `five minutes. You will receive an email at ${contactEmail} when the configuration is complete` : 'fifteen seconds'}.
</p>
</Alert>
)}
{untestedConfigs.length >= 1 && inProgressConfigs.length === 0 && (
<Alert
variant="warning"
icon={Warning}
className="ml-0 w-75"
onClose={closeAlerts}
dismissible
>
<Alert.Heading>You need to test your SSO connection</Alert.Heading>
<p>
Your SSO configuration has completed,
and you should have received an email with the following instructions:<br />
<br />
1. Copy the URL for your learner Portal dashboard below:<br />
<br />
&emsp; http://courses.edx.org/dashboard?tpa_hint=saml-bestrun-hana<br />
<br />
2: Launch a new incognito or private window and paste the copied URL into the URL bar to load your
learner Portal dashboard.<br />
<br />
3: When prompted, enter login credentials supported by your IDP to test your connection to edX.<br />
<br />
Return to this window after completing the testing instructions.
This window will automatically update when a successful test is detected.<br />
</p>
</Alert>
)}
{liveConfigs.length >= 1 && inProgressConfigs.length === 0 && untestedConfigs.length === 0 && (
<Alert
variant="success"
className="ml-0 w-75"
icon={CheckCircle}
onClose={closeAlerts}
dismissible
>
<Alert.Heading>Your SSO integration is live!</Alert.Heading>
<p>
Great news! Your test was successful and your new SSO integration is live and ready to use.
</p>
</Alert>
)}
</>
);

NewSSOConfigAlerts.propTypes = {
inProgressConfigs: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
untestedConfigs: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
liveConfigs: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
notConfigured: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
closeAlerts: PropTypes.func.isRequired,
contactEmail: PropTypes.string.isRequired,
};

const mapStateToProps = state => ({
contactEmail: state.portalConfiguration.contactEmail,
});

export default connect(mapStateToProps)(NewSSOConfigAlerts);
Loading

0 comments on commit 450bc0e

Please sign in to comment.