Introduction
In an organization, employees leave and join the organization and it is a process. Company data should be secured when employees leave the organization. Ideally, when an employee leaves, the IT Team should have the right to close and block the user by sign-in to the M365 environment.
We will discuss and learn about how we can implement a feature for the manager to block sign-in by users when any employee leaves the team.
Learning Objectives
- Information of Adaptive cards
- Information of Adaptive Cards Extension (ACEs)
- Generate SPFx solution with ACEs
- Create Model and Service
- Create multiple Quick View
- Modify default Card View
- Modify Adaptive Card Extension
- Output
Let’s begin.
Information of Adaptive Cards
Adaptive Cards is a universal Card UI framework released by Microsoft in 2017 which aims to standardize the lay-outing of Cards independent of the host platform.
The goal is to define a Card once, make it portable and flexible and use it to the different platforms to apply it according to its own requirements.
Adaptive Cards are written in JSON (JavaScript object notation). They’re just a simple syntax for how a Card should be structured, where the list goes, where a button sets, and how the title and description (information) looks, etc. Each host application that displays your Card will then tweak it so its styling looks like a native part of the app.
Reference Link: https://adaptivecards.io/
Information of Adaptive Cards Extension (ACEs)
Adaptive Card Extensions is a Component type in SharePoint Framework that can enable developers to create rich and native extensions for Microsoft Viva Connections’ Dashboards and SharePoint Pages. Aces are using the Adaptive Card engine to generate rich UI by applying declarative JSON schema.
Developers need to focus on only business logic and traverse Cards and Quick Views depending on business requirements.
Generate SPFx solution with ACEs
Before generating an SPFx solution with ACEs, make sure you have installed the below prerequisites.
Install the latest beta release using the below code.
npm install @microsoft/generator-sharepoint@next --global
Open node command prompt
Reach to your desire location where you want to create a solution
Run the below command
yo @microsoft/sharepoint
When prompted, enter the below values to each parameter.
Once all required packages are downloaded then you will get a message as below.
Now open a solution in visual studio code or editor that you want to use.
The below command is used to open the solution in VS Code
code .
Create Model and Service to consume Graph
Install required packages to connect graph in solution
npm install @pnp/sp @pnp/graph @pnp/logging
Create Model
Create a folder called “Models” in “..\src\”.
Create a new file with the name “models.ts” in folder models at location “..\src\models”.
Copy the below code and paste it into the file.
export interface IMyTeam {
userPrincipalName: string;
displayName: string;
givenName: string;
id: string;
mobilePhone: string;
officeLocation: string;
preferredLanguage: string;
surname: string;
jobTitle: string;
mail: string;
}
export interface IConfig {
members: IMyTeam[];
}
export class Config implements IConfig {
constructor(
public members: IMyTeam[] = []
) { }
}
Create Service
Create a folder called “Services” in “..\src\”.
Create a new file called “Service.ts” in folder services at location “..\src\services”.
Copy the below code to it.
import { IConfig, IMyTeam, Config } from "../models/models";
import { Logger, LogLevel } from "@pnp/logging";
import "@pnp/graph/users";
import "@pnp/graph/onedrive";
import "@pnp/graph/groups";
import { MSGraphClient } from "@microsoft/sp-http";
import { graph } from "@pnp/graph";
import forEach from "lodash/forEach";
export interface IService {
Init(client: MSGraphClient): Promise<void>;
}
export class Service implements IService {
private LOG_SOURCE: string = "🔶Service";
private _ready: boolean = false;
private _currentConfig: IConfig = null;
private _client: MSGraphClient;
public async Init(client: MSGraphClient): Promise<void> {
this._client = client;
await this._getConfig();
}
public async BlockSignIn(userPrincipalName): Promise<boolean> {
return await this._blockSignIn(userPrincipalName);
}
public get Ready(): boolean {
return this._ready;
}
public get Config(): IConfig {
return this._currentConfig;
}
private async _getConfig(): Promise<void> {
try {
this._currentConfig = await this.GenerateConfig();
this._ready = true;
} catch (error) {
Logger.write(`${this.LOG_SOURCE} (_getConfig) - ${error} - `, LogLevel.Error);
}
}
private async GenerateConfig(): Promise<IConfig> {
let mmpConfig: IConfig = null;
try {
mmpConfig = new Config();
mmpConfig.members = await this._directReportsToMe();
} catch (error) {
Logger.write(`${this.LOG_SOURCE} (GenerateConfig) - ${error} - `, LogLevel.Error);
}
return mmpConfig;
}
private async _directReportsToMe(): Promise<IMyTeam[]> {
let returnValue: IMyTeam[] = [];
try {
let directReports = await graph.me.directReports();
if (directReports.length > 0) {
forEach(directReports, (o: any) => {
returnValue.push({
displayName: o.displayName,
id: o.id,
jobTitle: o.jobTitle,
givenName: o.givenName,
mail: o.mail,
mobilePhone: o.mobilePhone,
officeLocation: o.officeLocation,
preferredLanguage: o.preferredLanguage,
surname: o.surname,
userPrincipalName: o.userPrincipalName
});
});
}
} catch (error) {
Logger.write(`${this.LOG_SOURCE} (_directReportsToMe) - ${error} - `, LogLevel.Error);
}
return returnValue;
}
private async _blockSignIn(userPrincipalName): Promise<boolean> {
let doesSignInBlocked: boolean = false;
await this._client.api(`/users/${userPrincipalName}`).patch({
"accountEnabled": false
}).then(() => {
doesSignInBlocked = true;
}).catch((error) => {
Logger.write(`${this.LOG_SOURCE} (_blockSignIn) - ${error} - `, LogLevel.Error);
});
return doesSignInBlocked;
}
}
Create Multiple Quick View
Success Quick View
Success quick view calls when block sign-in process has been successfully executed.
Create new file called “SuccessView.ts” at location “..\src\adaptiveCardExtensions\myTeam\quickView \”
Copy below code in the “SuccessView.ts” file.
import { ISPFxAdaptiveCard, BaseAdaptiveCardView, IActionArguments } from '@microsoft/sp-adaptive-card-extension-base';
import { IMyTeamAdaptiveCardExtensionProps, IMyTeamAdaptiveCardExtensionState } from '../MyTeamAdaptiveCardExtension';
export interface ISuccessViewData {
subTitle: string;
title: string;
description: string;
}
export class SuccessView extends BaseAdaptiveCardView<
IMyTeamAdaptiveCardExtensionProps,
IMyTeamAdaptiveCardExtensionState, ISuccessViewData> {
public get data(): ISuccessViewData {
return {
subTitle: `Block Sign-In is successfully done!!`,
title: `User : ${this.state.currentConfig.members[this.state.currentIndex].displayName} `,
description: `${this.state.currentConfig.members[this.state.currentIndex].displayName} will not be able to sign in...`,
};
}
public get template(): ISPFxAdaptiveCard {
return {
"schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.2",
"body": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": "${title}"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": "${subTitle}",
"wrap": true
}
]
}
]
},
{
"type": "TextBlock",
"text": "${description}",
"wrap": true
}
],
"actions": [
{
"type": "Action.Submit",
"id": "close",
"title": "Close",
},
]
};
}
public onAction(action: IActionArguments): void {
if (action.id == "close") {
this.quickViewNavigator.close();
}
}
}
Error Quick View
Error quick view call when block sign-in process has not been successfully executed.
Create new file called “ErrorView.ts” at location “...\src\adaptiveCardExtensions\myTeam\quickView \”
Copy below code in the “ErrorView.ts” file.
import { ISPFxAdaptiveCard, BaseAdaptiveCardView, IActionArguments } from '@microsoft/sp-adaptive-card-extension-base';
import { IMyTeamAdaptiveCardExtensionProps, IMyTeamAdaptiveCardExtensionState } from '../MyTeamAdaptiveCardExtension';
export interface IErrorViewData {
subTitle: string;
title: string;
description: string;
}
export class ErrorView extends BaseAdaptiveCardView<
IMyTeamAdaptiveCardExtensionProps,
IMyTeamAdaptiveCardExtensionState, IErrorViewData> {
public get data(): IErrorViewData {
return {
subTitle: "Error Occured while blocking user...",
title: `User : ${this.state.currentConfig.members[this.state.currentIndex].displayName} `,
description: "Authorization_RequestDenied : Insufficient privileges to complete the operation.",
};
}
public get template(): ISPFxAdaptiveCard {
return {
"schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.2",
"body": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": "${title}"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": "${subTitle}",
"wrap": true
}
]
}
]
},
{
"type": "TextBlock",
"text": "${description}",
"wrap": true
}
],
"actions": [
{
"type": "Action.Submit",
"id": "close",
"title": "Close",
},
]
};
}
public onAction(action: IActionArguments): void {
if (action.id == "close") {
this.quickViewNavigator.pop();
}
}
}
Quick View
Quick View is called when user acts on card view.
Open file “QuickView.ts” which is on location “...\src\adaptiveCardExtensions\myTeam\quickView \”
Copy the below code and paste it into that file.
import { ISPFxAdaptiveCard, BaseAdaptiveCardView, IActionArguments } from '@microsoft/sp-adaptive-card-extension-base';
import * as strings from 'MyTeamAdaptiveCardExtensionStrings';
import { IMyTeam } from '../../../models/models';
import { IMyTeamAdaptiveCardExtensionProps, IMyTeamAdaptiveCardExtensionState,ERROR_VIEW_REGISTRY_ID,SUCCESS_VIEW_REGISTRY_ID} from '../MyTeamAdaptiveCardExtension';
export interface IQuickViewData {
subTitle: string;
title: string;
description: string;
item: IMyTeam;
}
export class QuickView extends BaseAdaptiveCardView<
IMyTeamAdaptiveCardExtensionProps,
IMyTeamAdaptiveCardExtensionState,
IQuickViewData
> {
public get data(): IQuickViewData {
return {
subTitle: strings.SubTitle,
title: strings.Title,
description: this.properties.description,
item: this.state.currentConfig.members[this.state.currentIndex]
};
}
public get template(): ISPFxAdaptiveCard {
return {
"type": "AdaptiveCard",
"version": "1.2",
"body": [
{
"type": "Container",
"$data": "${item}",
"items": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": "Display Name: ${displayName}",
"color": "attention"
},
{
"type": "Container",
"spacing": "Small",
"items": [
{
"type": "TextBlock",
"text": "Surname: ${surname}",
"spacing": "Small"
},
{
"type": "TextBlock",
"text": "Given Name: ${givenName}",
"spacing": "Small"
},
{
"type": "TextBlock",
"text": "Mail: ${mail}",
"spacing": "Small",
"color": "good",
}
]
}
],
"separator": true
}
],
"actions": [
{
"type": "Action.Submit",
"id": "close",
"title": "Close",
},
{
"type": "Action.Submit",
"id": "Block",
"title": "Block Sign-In",
},
{
"type": "Action.Submit",
"id": "Error",
"title": "Generate Error",
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json"
};
//return require('./template/QuickViewTemplate.json');
}
public onAction(action: IActionArguments): void {
if (action.id == "close") {
this.quickViewNavigator.close();
}
if (action.id == "Block") {
setTimeout(async () => {
await this.state.service.BlockSignIn(this.state.currentConfig.members[this.state.currentIndex].userPrincipalName)
.then((res) => {
if(res==false){
// Push Error card to quick view navigator
this.quickViewNavigator.push(ERROR_VIEW_REGISTRY_ID);
}
else{
this.quickViewNavigator.push(SUCCESS_VIEW_REGISTRY_ID);
}
}).catch((error) => {
alert("Something went wrong");
});
}, 0);
}
if(action.id == 'Error'){
this.quickViewNavigator.push(ERROR_VIEW_REGISTRY_ID);
}
}
}
Create index.ts file at location “...\src\adaptiveCardExtensions\myTeam\quickView \” and pase below code in that.
export * from "./ErrorView";
export * from "./QuickView";
export * from "./SuccessView";
The structure would be as below.
Modify default Card View
Open “Cardview.ts” file from location “..src\adaptiveCardExtensions\myTeam\cardView” and copy below code in that file.
import {
BaseBasicCardView,
IBasicCardParameters,
IExternalLinkCardAction,
IQuickViewCardAction,
ICardButton,
IActionArguments
} from '@microsoft/sp-adaptive-card-extension-base';
import * as strings from 'MyTeamAdaptiveCardExtensionStrings';
import { IMyTeamAdaptiveCardExtensionProps, IMyTeamAdaptiveCardExtensionState, QUICK_VIEW_REGISTRY_ID } from '../MyTeamAdaptiveCardExtension';
export class CardView extends BaseBasicCardView<IMyTeamAdaptiveCardExtensionProps, IMyTeamAdaptiveCardExtensionState> {
/**
* Add multiple button on card view
*/
public get cardButtons(): [ICardButton] | [ICardButton, ICardButton] | undefined {
const buttons: ICardButton[] = [];
if (this.state.currentIndex > 0) {
buttons.push({
title: 'Previous',
action: {
type: 'Submit',
parameters: {
id: 'previous',
op: -1
}
}
});
}
if (this.state.currentIndex < this.state.currentConfig.members.length - 1) {
buttons.push({
title: 'Next',
action: {
type: 'Submit',
parameters: {
id: 'next',
op: 1 // Increment the index
}
}
});
}
return buttons as [ICardButton] | [ICardButton, ICardButton];
}
/**
* Handle action when user click on card view
* @param action
*/
public onAction(action: IActionArguments): void {
if (action.type === 'Submit') {
const { id, op } = action.data;
switch (id) {
case 'previous':
case 'next':
this.setState({ currentIndex: this.state.currentIndex + op });
break;
}
}
}
/**
* get Data and display it in Card View
*/
public get data(): IBasicCardParameters {
const { mail,displayName } = this.state.currentConfig.members[this.state.currentIndex];
return {
primaryText: `Display Name: ${displayName} | mail: ${mail}`,
};
}
/**
* Call Quick View
*/
public get onCardSelection(): IQuickViewCardAction | IExternalLinkCardAction | undefined {
return {
type: 'QuickView',
parameters: {
view: QUICK_VIEW_REGISTRY_ID,
}
};
}
}
Modify Adaptive Card Extension File
Open file “MyTeamAdaptiveCardExtension.ts” file at location “..src\adaptiveCardExtensions\myTeam”.
Copy the below code and paste it Inf the file “MyTeamAdaptiveCardExtension.ts”.
import { IPropertyPaneConfiguration } from '@microsoft/sp-property-pane';
import { BaseAdaptiveCardExtension } from '@microsoft/sp-adaptive-card-extension-base';
import { CardView } from './cardView/CardView';
import { QuickView,ErrorView,SuccessView } from "./quickView/index";
import { MyTeamPropertyPane } from './MyTeamPropertyPane';
import { IConfig } from "../../models/models";
import { Service } from "../../services/service";
import { Logger, LogLevel } from '@pnp/logging/logger';
import { ConsoleListener } from '@pnp/logging/listeners';
import { graph } from '@pnp/graph';
import { MSGraphClient } from "@microsoft/sp-http";
export interface IMyTeamAdaptiveCardExtensionProps {
title: string;
description: string;
iconProperty: string;
}
/**
* Add state property to handle multiple items in Card View
*/
export interface IMyTeamAdaptiveCardExtensionState {
currentIndex:number;
currentConfig: IConfig;
service: Service;
}
/**
* Initlize Register ID for Quick View and Card View.
*/
const CARD_VIEW_REGISTRY_ID: string = 'MyTeam_CARD_VIEW';
export const QUICK_VIEW_REGISTRY_ID: string = 'MyTeam_QUICK_VIEW';
export const ERROR_VIEW_REGISTRY_ID: string = 'MyTeam_ERROR_VIEW';
export const SUCCESS_VIEW_REGISTRY_ID:string = 'MyTeam_SUCCESS_VIEW';
export default class MyTeamAdaptiveCardExtension extends BaseAdaptiveCardExtension<
IMyTeamAdaptiveCardExtensionProps,
IMyTeamAdaptiveCardExtensionState
> {
// Private variables
private LOG_SOURCE: string = "🔶MyTeamAdaptiveCardExtension";
private _deferredPropertyPane: MyTeamPropertyPane | undefined;
private service: Service = new Service();
private _client = MSGraphClient;
/** onInit functiont get records and store it in config file. */
/**
* Inilize service and it's method to use in quick view and card view
* @returns
*/
public async onInit(): Promise<void> {
Logger.subscribe(new ConsoleListener());
Logger.activeLogLevel = LogLevel.Info;
try {
graph.setup({ spfxContext: this.context });
const client = await this.context.msGraphClientFactory.getClient();
await this.service.Init(client);
this.state = {
currentIndex:0,
currentConfig: this.service.Config,
service:this.service
};
} catch (error) {
Logger.write(`${this.LOG_SOURCE} (_directReportsToMe) - ${error} - `, LogLevel.Error);
}
// Register CardView and QuickView to Card navigator and quick view navigator.
this.cardNavigator.register(CARD_VIEW_REGISTRY_ID, () => new CardView());
this.quickViewNavigator.register(QUICK_VIEW_REGISTRY_ID, () => new QuickView());
this.quickViewNavigator.register(ERROR_VIEW_REGISTRY_ID, () => new ErrorView());
this.quickViewNavigator.register(SUCCESS_VIEW_REGISTRY_ID, () => new SuccessView());
return Promise.resolve();
}
public get title(): string {
return this.properties.title;
}
protected get iconProperty(): string {
return this.properties.iconProperty || require('./assets/microsoft-teams.svg');
}
protected loadPropertyPaneResources(): Promise<void> {
return import(
/* webpackChunkName: 'MyTeam-property-pane'*/
'./MyTeamPropertyPane'
)
.then(
(component) => {
this._deferredPropertyPane = new component.MyTeamPropertyPane();
}
);
}
protected renderCard(): string | undefined {
return CARD_VIEW_REGISTRY_ID;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return this._deferredPropertyPane!.getPropertyPaneConfiguration();
}
}
Output
Open a terminal and run the below command
gulp serve -l
Open URL: https://dev1802.sharepoint.com/_layouts/15/workbench.aspx and select webpart from Dashboard group.
Reference Links