| | import React from 'react';
import {
Dialog,
DialogType,
IDialogContentProps,
DialogFooter,
PrimaryButton,
DefaultButton,
Stack,
Text,
Icon,
Spinner,
SpinnerSize,
MessageBar,
MessageBarType,
Separator
} from '@fluentui/react';
import { AuthService } from 'services/AuthService';
import { TokenKind } from 'shared/TokenKind';
import { TextService } from 'services/TextService';
import strings from 'VistoWebPartStrings';
import { useTheme } from '@fluentui/react';
import { trackClient } from 'services/trackClient';
export interface IConsentDialogProps {
onDismiss: () => void;
}
interface IConsentItem {
key: string;
name: string;
description: string;
tokenKind: TokenKind;
domain?: string;
isGranted: boolean;
isChecking: boolean;
error?: string;
}
export const ConsentDialog: React.FC<IConsentDialogProps> = (props) => {
const theme = useTheme();
const [consentItems, setConsentItems] = React.useState<IConsentItem[]>([
{
key: 'default',
name: TextService.format(strings.ConsentDialog_Default),
description: TextService.format(strings.ConsentDialog_DefaultDescription),
tokenKind: TokenKind.dashboard,
domain: 'graph.microsoft.com',
isGranted: false,
isChecking: false
},
{
key: 'sharepoint',
name: TextService.format(strings.ConsentDialog_SharePoint),
description: TextService.format(strings.ConsentDialog_SharePointDescription),
tokenKind: TokenKind.sharepoint,
isGranted: false,
isChecking: false
},
{
key: 'devops',
name: TextService.format(strings.ConsentDialog_DevOps),
description: TextService.format(strings.ConsentDialog_DevOpsDescription),
tokenKind: TokenKind.devops,
domain: 'dev.azure.com',
isGranted: false,
isChecking: false
},
{
key: 'planner',
name: TextService.format(strings.ConsentDialog_Planner),
description: TextService.format(strings.ConsentDialog_PlannerDescription),
tokenKind: TokenKind.planner,
domain: 'graph.microsoft.com',
isGranted: false,
isChecking: false
},
{
key: 'excel',
name: TextService.format(strings.ConsentDialog_Excel),
description: TextService.format(strings.ConsentDialog_ExcelDescription),
tokenKind: TokenKind.excel,
domain: 'graph.microsoft.com',
isGranted: false,
isChecking: false
}
]);
const [isInitializing, setIsInitializing] = React.useState(true);
const formatErrorMessage = (error: any, item: IConsentItem): string => {
const errorMessage = error?.message || error?.toString() || 'Unknown error';
// Special handling for DevOps AADSTS650052 error
if (item.key === 'devops' && errorMessage.includes('AADSTS650052')) {
return "DevOps may not be available for your organization or user account. This is a common reason for this error.";
}
return errorMessage;
};
const checkConsentStatus = async (item: IConsentItem): Promise<boolean> => {
try {
// Try to get a token - if it succeeds, consent is granted
const domain = item.domain || 'graph.microsoft.com';
await AuthService.getAuthToken(item.tokenKind, domain);
return true;
} catch (error) {
// If we get a consent error, consent is not granted
trackClient.warn(`Consent check failed for ${TokenKind[item.tokenKind]}`, error);
return false;
}
};
const updateConsentItem = (key: string, updates: Partial<IConsentItem>) => {
setConsentItems(prev => prev.map(item =>
item.key === key ? { ...item, ...updates } : item
));
};
const checkAllConsents = async () => {
setIsInitializing(true);
const checkPromises = consentItems.map(async (item) => {
updateConsentItem(item.key, { isChecking: true, error: undefined });
try {
const isGranted = await checkConsentStatus(item);
updateConsentItem(item.key, {
isGranted,
isChecking: false,
error: undefined
});
} catch (error) {
updateConsentItem(item.key, {
isGranted: false,
isChecking: false,
error: formatErrorMessage(error, item)
});
}
});
await Promise.all(checkPromises);
setIsInitializing(false);
};
React.useEffect(() => {
checkAllConsents();
}, []);
const requestConsent = async (item: IConsentItem) => {
if (!AuthService.getConsent) {
trackClient.error('getConsent provider not available');
return;
}
updateConsentItem(item.key, { isChecking: true, error: undefined });
try {
const domain = item.domain || 'graph.microsoft.com';
await AuthService.getConsent(item.tokenKind, async () => {
// Test callback to verify consent was granted
await AuthService.getAuthToken(item.tokenKind, domain);
}, domain);
// If we get here, consent was granted
updateConsentItem(item.key, {
isGranted: true,
isChecking: false,
error: undefined
});
} catch (error) {
updateConsentItem(item.key, {
isGranted: false,
isChecking: false,
error: formatErrorMessage(error, item)
});
}
};
const getStatusIcon = (item: IConsentItem) => {
if (item.isChecking) {
return <Spinner size={SpinnerSize.small} />;
}
if (item.isGranted) {
return <Icon iconName="CheckMark" style={{ color: theme.palette.green }} />;
}
return <Icon iconName="ErrorBadge" style={{ color: theme.palette.red }} />;
};
const contentProps: IDialogContentProps = {
type: DialogType.largeHeader,
title: TextService.format(strings.ConsentDialog_Title),
subText: TextService.format(strings.ConsentDialog_Description)
};
const grantedCount = consentItems.filter(item => item.isGranted).length;
const totalCount = consentItems.length;
return (
<Dialog
minWidth={400}
maxWidth={600}
isBlocking={false}
dialogContentProps={contentProps}
isOpen={true}
onDismiss={props.onDismiss}
>
<Stack tokens={{ childrenGap: 'l1' }} styles={{ root: { paddingBottom: '32px' } }}>
{isInitializing && (
<MessageBar messageBarType={MessageBarType.info}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 's1' }}>
<Spinner size={SpinnerSize.small} />
<Text>{TextService.format(strings.ConsentDialog_CheckingAll)}</Text>
</Stack>
</MessageBar>
)}
{!isInitializing && (
<MessageBar
messageBarType={grantedCount === totalCount ? MessageBarType.success : MessageBarType.warning}
>
<Text>
{TextService.format(strings.ConsentDialog_Status, {
granted: grantedCount.toString(),
total: totalCount.toString()
})}
</Text>
</MessageBar>
)}
<Stack tokens={{ childrenGap: 's1' }}>
{consentItems.map((item, index) => (
<div key={item.key}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 'm' }}>
<div style={{ width: 20, flexShrink: 0 }}>
{getStatusIcon(item)}
</div>
<Stack grow tokens={{ childrenGap: 'xs' }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 's2' }}>
<Text variant="medium" style={{ fontWeight: 600 }}>
{item.name}
</Text>
{(!item.isGranted && !item.isChecking) && (
<Text variant="small" style={{
color: theme.palette.orange,
backgroundColor: theme.palette.neutralLighter,
padding: '2px 6px',
borderRadius: '2px',
fontSize: '11px',
fontWeight: 600
}}>
{TextService.format(strings.ConsentDialog_NotGranted)}
</Text>
)}
</Stack>
<Text variant="small" style={{ color: theme.palette.neutralPrimary }}>
{item.description}
</Text>
{item.error && (
<Text variant="small" style={{
color: theme.palette.red,
wordBreak: 'break-word',
maxWidth: '100%'
}}>
{TextService.format(strings.ConsentDialog_Error)}: {item.error}
</Text>
)}
</Stack>
<Stack horizontal verticalAlign="center" style={{ minWidth: 140, justifyContent: 'flex-end' }}>
{item.isGranted ? (
<DefaultButton
disabled={true}
text={TextService.format(strings.ConsentDialog_Granted)}
iconProps={{ iconName: 'CheckMark' }}
styles={{
root: {
backgroundColor: theme.palette.neutralLighter,
borderColor: theme.palette.neutralLight,
color: theme.palette.neutralSecondary
},
icon: { color: theme.palette.green }
}}
/>
) : item.isChecking ? (
<DefaultButton
disabled={true}
text={TextService.format(strings.ConsentDialog_Checking)}
onRenderIcon={() => <Spinner size={SpinnerSize.xSmall} />}
styles={{
root: {
backgroundColor: theme.palette.neutralLighter,
borderColor: theme.palette.neutralLight,
color: theme.palette.neutralSecondary
}
}}
/>
) : (
<PrimaryButton
text={TextService.format(strings.ConsentDialog_GrantConsent)}
onClick={() => requestConsent(item)}
/>
)}
</Stack>
</Stack>
{index < consentItems.length - 1 && <Separator styles={{ root: { margin: '4px 0' } }} />}
</div>
))}
</Stack>
</Stack>
<DialogFooter styles={{ actions: { display: 'flex', flexGrow: 1 }, actionsRight: { flexGrow: 1, justifyContent: 'space-between' } }}>
<DefaultButton
text={TextService.format(strings.ConsentDialog_RefreshAll)}
onClick={checkAllConsents}
disabled={isInitializing}
iconProps={{ iconName: 'Refresh' }}
/>
<DefaultButton onClick={props.onDismiss} text={TextService.format(strings.ConsentDialog_Close)} />
</DialogFooter>
</Dialog>
);
};
|