Compilación de una aplicación de pestaña de panel
Un panel es una herramienta para realizar un seguimiento, analizar y mostrar datos para obtener información sobre una organización o un proceso específico. Los paneles de Teams permiten supervisar y ver métricas importantes.
La plantilla de pestaña panel del kit de herramientas de Teams le permite empezar a integrar un lienzo con varias tarjetas que proporcionan información general sobre el contenido en Teams. Puede:
- Use widgets para mostrar contenido de aplicaciones y servicios en la pestaña del panel.
- Integre la aplicación con Graph API para visualizar detalles sobre la implementación de los datos seleccionados.
- Cree paneles personalizables que permitan a su empresa establecer objetivos específicos que le ayuden a realizar un seguimiento de la información que necesita ver en varias áreas y entre departamentos.
El equipo puede obtener las actualizaciones más recientes de diferentes orígenes en Teams mediante la aplicación de pestaña panel de Teams. Use aplicaciones de pestaña de panel para conectar numerosas métricas, orígenes de datos, API y servicios. Las aplicaciones de pestaña del panel ayudan a su empresa a extraer información relevante de los orígenes y a presentarla a los usuarios. Para obtener más información sobre cómo crear una aplicación de pestaña de panel, consulte la guía paso a paso.
Agregar un nuevo panel
Después de crear una aplicación de pestaña de panel, puede agregar un nuevo panel.
Para agregar un nuevo panel, siga estos pasos:
- Creación de una clase de panel
- Invalidar métodos para personalizar la aplicación de pestaña del panel
- Agregar una ruta para la nueva aplicación de pestaña de panel
- Modificación del manifiesto para agregar una nueva aplicación de pestaña de panel
Creación de una clase de panel
Cree un archivo con la .tsx
extensión del panel en el src/dashboards
directorio, por ejemplo, YourDashboard.tsx
. A continuación, cree una clase que extienda el BaseDashboard class from
@microsoft/teamsfx-react.
// Create a dashboard class - https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/build-a-dashboard-tab-app#create-a-dashboard-class
import { BaseDashboard } from "@microsoft/teamsfx-react";
export default class SampleDashboard extends BaseDashboard { }
Nota:
Todos los métodos son opcionales. Si no invalida ningún método, se usa el diseño de panel predeterminado.
Invalidar métodos para personalizar la aplicación de pestaña del panel
La BaseDashboard
clase proporciona algunos métodos que puede invalidar para personalizar el diseño del panel. En la tabla siguiente se enumeran los métodos que puede invalidar:
Métodos | Función |
---|---|
styling() |
Personalice el estilo del panel. |
layout() |
Definir el diseño de widgets. |
El código siguiente es un ejemplo para personalizar el diseño del panel:
.your-dashboard-layout {
grid-template-columns: 6fr 4fr;
}
import { BaseDashboard } from "@microsoft/teamsfx-react";
import ListWidget from "../widgets/ListWidget";
import ChartWidget from "../widgets/ChartWidget";
export default class YourDashboard extends BaseDashboard {
styling() {
return "your-dashboard-layout";
}
layout() {
return (
<>
<ListWidget />
<ChartWidget />
</>
);
}
}
Agregar una ruta para la nueva aplicación de pestaña de panel
Debe vincular el widget a un archivo de origen de datos. El widget recoge los datos que se presentan en el panel desde el archivo de origen.
Abra el src/App.tsx
archivo y agregue una ruta para el nuevo panel. Aquí le mostramos un ejemplo:
import YourDashboard from "./dashboards/YourDashboard";
export default function App() {
...
<Route path="/yourdashboard" element={<yourdashboard />} />
...
}
Modificación del manifiesto para agregar una nueva aplicación de pestaña de panel
Abra el appPackage/manifest.json
archivo y agregue una nueva pestaña de panel en staticTabs
. Para obtener más información, vea manifiesto de aplicación. Aquí le mostramos un ejemplo:
{
"entityId": "index1",
"name": "Your Dashboard",
"contentUrl": "${{TAB_ENDPOINT}}/index.html#/yourdashboard",
"websiteUrl": "${{TAB_ENDPOINT}}/index.html#/yourdashboard",
"scopes": ["personal"]
}
Personalización del diseño del panel
TeamsFx proporciona métodos prácticos para definir y modificar el diseño del panel. Estos son los métodos:
Tres widgets en una fila con el alto de 350 px ocupando el 20 por ciento, el 60 por ciento y el 20 por ciento del ancho, respectivamente.
.customize-class-name { grid-template-rows: 350px; grid-template-columns: 2fr 6fr 2fr; }
export default class SampleDashboard extends BaseDashboard { styling() { return "customize-class-name"; } layout() { return ( <> <ListWidget /> <ChartWidget /> <NewsWidget /> </> ); } }
Dos widgets en una fila con un ancho de 600 px y 1100 px. El alto de la primera línea es el alto máximo de su contenido y el alto de la segunda línea es de 400 px.
.customize-class-name { grid-template-rows: max-content 400px; grid-template-columns: 600px 1100px; }
export default class SampleDashboard extends Dashboard { styling() { return "customize-class-name"; } layout() { return ( <> <ListWidget /> <ChartWidget /> <NewsWidget /> </> ); } }
Organice dos widgets en una columna.
.one-column { display: grid; gap: 20px; grid-template-rows: 1fr 1fr; }
export default class SampleDashboard extends BaseDashboard { layout() { return ( <> <ListWidget /> <ChartWidget /> </> ); } }
Abstracción de la aplicación de pestaña panel
Para ajustar el diseño del panel, TeamsFx proporciona una BaseDashboard
clase para que los desarrolladores implementen un panel.
El código siguiente es un ejemplo de una BaseDashboard
clase:
function dashboardStyle(isMobile?: boolean) {
return mergeStyles({
display: "grid",
gap: "20px",
padding: "20px",
gridTemplateRows: "1fr",
gridTemplateColumns: "4fr 6fr",
...(isMobile === true ? { gridTemplateColumns: "1fr", gridTemplateRows: "1fr" } : {}),
});
}
interface BaseDashboardState {
isMobile?: boolean;
showLogin?: boolean;
observer?: ResizeObserver;
}
export class BaseDashboard<P, S> extends Component<P, S & BaseDashboardState> {
private ref: React.RefObject<HTMLDivElement>;
public constructor(props: Readonly<P>) {
super(props);
this.state = {
isMobile: undefined,
showLogin: undefined,
observer: undefined,
} as S & BaseDashboardState;
this.ref = React.createRef<HTMLDivElement>();
}
public async componentDidMount() {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target === this.ref.current) {
const { width } = entry.contentRect;
this.setState({ isMobile: width < 600 } as S & BaseDashboardState);
}
}
});
observer.observe(this.ref.current!);
}
public componentWillUnmount(): void {
if (this.state.observer && this.ref.current) {
this.state.observer.unobserve(this.ref.current);
}
}
public render() {
return (
<div
ref={this.ref}
className={mergeStyles(dashboardStyle(this.state.isMobile), this.styling())}
>
{this.layout()}
</div>
);
}
protected layout(): JSX.Element | undefined {
return undefined;
}
protected styling(): string {
return null;
}
}
En la BaseDashboard
clase, TeamsFx proporciona diseños básicos con métodos personalizables. El panel sigue siendo un componente react y TeamsFx proporciona implementaciones básicas de funciones basadas en el ciclo de vida de los componentes react, como:
- Implementación de una lógica de representación básica basada en el diseño de cuadrícula.
- Agregar un observador para adaptarse automáticamente a los dispositivos móviles.
A continuación se muestran los métodos personalizables para invalidar:
Métodos | Función | Recomendación para invalidar |
---|---|---|
constructor() |
Inicializa el estado del panel y las variables. | No |
componentDidMount() |
Invoca después de montar un componente. | No |
componentWillUnmount() |
Invoca cuando se desmonta un componente. | No |
render() |
Invoca cuando hay una actualización. El diseño predeterminado del panel se define en este método. | No |
layout |
Define el diseño del widget en el panel. Puede invalidar este método. | Yes |
styling() |
Para personalizar el estilo del panel. Puede invalidar este método. | Yes |
Uso de un widget en el panel
Los widgets muestran información configurable y gráficos en los paneles. Aparecen en el panel de widgets, donde puede anclar, desanclar, organizar, cambiar el tamaño y personalizar widgets para reflejar sus intereses. El panel de widgets está optimizado para mostrar widgets relevantes y contenido personalizado en función de su uso.
Personalización del widget
Puede personalizar el widget reemplazando los métodos siguientes en la BaseWidget
clase :
Invalide
header()
,body()
yfooter()
para personalizar el widget.export class NewsWidget extends BaseWidget<any, any> { override header(): JSX.Element | undefined { return ( <div> <News28Regular /> <Text>Your News</Text> <Button icon={<MoreHorizontal32Regular />} appearance="transparent" /> </div> ); } override body(): JSX.Element | undefined { return ( <div> <Image src="image.svg" /> <Text>Lorem Ipsum Dolor</Text> <Text> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Enim, elementum sed </Text> </div> ); } override footer(): JSX.Element | undefined { return ( <Button appearance="transparent" icon={<ArrowRight16Filled />} iconPosition="after" size="small" > View details </Button> ); } }
Invalide
body()
yfooter()
para personalizar el widget.export class NewsWidget extends BaseWidget<any, any> { override body(): JSX.Element | undefined { return ( <div> <Image src="image.svg" /> <Text>Lorem Ipsum Dolor</Text> <Text> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Enim, elementum sed </Text> </div> ); } override footer(): JSX.Element | undefined { return ( <Button appearance="transparent" icon={<ArrowRight16Filled />} iconPosition="after" size="small" > View details </Button> ); } }
Invalide
body()
para personalizar el widget.export class NewsWidget extends BaseWidget<any, any> { override body(): JSX.Element | undefined { return ( <div> <Image src="image.svg" /> <Text>Lorem Ipsum Dolor</Text> <Text> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Enim, elementum sed </Text> </div> ); } }
Incluir un cargador de datos
Si desea incluir un cargador de datos en el widget antes de cargar el widget, puede agregar una propiedad al estado del widget para indicar que el cargador de datos es loading()
. Puede usar esta propiedad para mostrar un indicador de carga al usuario.
Ejemplo:
override loading(): JSX.Element | undefined {
return (
<div className="loading">
<Spinner label="Loading..." labelPosition="below" />
</div>
);
}
Ahora, se muestra el spinner de carga mientras se cargan los datos. Cuando se cargan los datos, el spinner de carga está oculto y se muestran los datos de lista y el botón de pie de página.
Controlar el estado vacío
Puede mostrar un contenido específico en el widget cuando los datos están vacíos. Para ello, debe modificar el método en el body
archivo de widget para adoptar diferentes estados de los datos.
En el ejemplo siguiente se muestra cómo mostrar una imagen vacía cuando los datos de ListWidget están vacíos .
override body(): JSX.Element | undefined {
let hasData = this.state.data && this.state.data.length > 0;
return (
<div>
{hasData ? (
<>
{this.state.data?.map((t: ListModel) => {
...
})}
</>
) : (
<div>
<Image src="empty-default.svg" height="150px" />
<Text align="center">No data</Text>
</div>
)}
</div>
);
}
Puede usar un enfoque similar para quitar el contenido del pie de página del widget cuando los datos están vacíos.
override footer(): JSX.Element | undefined {
let hasData = this.state.data && this.state.data.length > 0;
if (hasData) {
return <Button>...</Button>;
}
}
Cuando los datos están vacíos, el widget de lista aparece de la siguiente manera:
Actualizar datos según lo programado
En el ejemplo siguiente se muestra cómo mostrar datos en tiempo real en un widget. El widget muestra la hora actual y las actualizaciones.
interface IRefreshWidgetState {
data: string;
}
export class RefreshWidget extends BaseWidget<any, IRefreshWidgetState> {
override body(): JSX.Element | undefined {
return <>{this.state.data}</>;
}
async componentDidMount() {
setInterval(() => {
this.setState({ data: new Date().toLocaleTimeString() });
}, 1000);
}
}
Puede modificar setInterval
el método para llamar a su propia función para actualizar datos como este setInterval(() => yourGetDataFunction(), 1000)
.
Abstracción de widget
Para simplificar el desarrollo de un widget, el SDK de TeamsFx proporciona una BaseWidget
clase para que los desarrolladores hereden para implementar un widget que satisfaga sus necesidades sin prestar mucha atención a la implementación del diseño del widget.
El código siguiente es un ejemplo de la clase BaseWidget:
export interface IWidgetClassNames {
root?: string;
header?: string;
body?: string;
footer?: string;
}
const classNames: IWidgetClassNames = mergeStyleSets({
root: {
display: "grid",
padding: "1.25rem 2rem 1.25rem 2rem",
backgroundColor: tokens.colorNeutralBackground1,
border: "1px solid var(--colorTransparentStroke)",
boxShadow: tokens.shadow4,
borderRadius: tokens.borderRadiusMedium,
gap: tokens.spacingHorizontalL,
gridTemplateRows: "max-content 1fr max-content",
},
header: {
display: "grid",
height: "max-content",
"& div": {
display: "grid",
gap: tokens.spacingHorizontalS,
alignItems: "center",
gridTemplateColumns: "min-content 1fr min-content",
},
"& svg": {
height: "1.5rem",
width: "1.5rem",
},
"& span": {
fontWeight: tokens.fontWeightSemibold,
lineHeight: tokens.lineHeightBase200,
fontSize: tokens.fontSizeBase200,
},
},
footer: {
"& button": {
width: "fit-content",
},
},
});
interface BaseWidgetState {
loading?: boolean;
}
export class BaseWidget<P, S> extends Component<P, S & BaseWidgetState> {
public constructor(props: Readonly<P>) {
super(props);
this.state = { loading: undefined } as S & BaseWidgetState;
}
public async componentDidMount() {
this.setState({ ...(await this.getData()), loading: false });
}
public render() {
const { root, header, body, footer } = this.styling();
const showLoading = this.state.loading !== false && this.loading() !== undefined;
return (
<div className={mergeStyles(classNames.root, root)}>
{this.header() && (
<div className={mergeStyles(classNames.header, header)}>{this.header()}</div>
)}
{showLoading ? (
this.loading()
) : (
<>
{this.body() !== undefined && <div className={body}>{this.body()}</div>}
{this.footer() !== undefined && (
<div className={mergeStyles(classNames.footer, footer)}>{this.footer()}</div>
)}
</>
)}
</div>
);
}
protected async getData(): Promise<S> {
return undefined;
}
protected header(): JSX.Element | undefined {
return undefined;
}
protected body(): JSX.Element | undefined {
return undefined;
}
protected footer(): JSX.Element | undefined {
return undefined;
}
protected loading(): JSX.Element | undefined {
return undefined;
}
protected styling(): IWidgetClassNames {
return {};
}
}
Los siguientes son los métodos recomendados para invalidar:
Métodos | Función | Recomendación para invalidar |
---|---|---|
constructor() |
Invoca la inicial this.state y llama al constructor de la superclase React.Component . |
No |
componentDidMount() |
Invoca después de montar un componente y asigna un valor a la data propiedad del estado llamando al getData() método . |
No |
render() |
Invoca cada vez que hay una actualización. El diseño predeterminado del panel se define en este método. | No |
getData() |
Invoca los datos necesarios para el widget. El valor devuelto por este método se establece en this.state.data . |
Yes |
header() |
Invoca el aspecto del encabezado del widget. Puede optar por invalidar este método para personalizar un widget o no, si no es así, el widget no tendrá un encabezado. | Yes |
body() |
Invoca el aspecto del cuerpo del widget. Puede optar por invalidar este método para personalizar un widget o no, si no es así, el widget no tendrá un cuerpo. | Yes |
footer() |
Invoca el aspecto del pie de página del widget. Puede optar por invalidar este método para personalizar un widget o no, si no es así, el widget no tendrá un pie de página. | Yes |
loading() |
Invoca cuando el widget está en proceso de captura de datos. Si se requiere un indicador de carga, el método puede devolver un JSX.Element que contenga los componentes necesarios para representar el indicador de carga. |
Yes |
style() |
Invoca un objeto que define los nombres de clase para las distintas partes del widget. | Yes |
Microsoft Graph Toolkit como contenido de widget
Microsoft Graph Toolkit es un conjunto de componentes web renovables independientes del marco, que ayuda a acceder y trabajar con Microsoft Graph. Puede usar el kit de herramientas de Microsoft Graph con cualquier marco web o sin un marco.
Para usar Microsoft Graph Toolkit como contenido del widget, siga estos pasos:
Agregar la característica sso a la aplicación de Teams: Microsoft Teams proporciona una función de inicio de sesión único (SSO) para que una aplicación obtenga el token de usuario de Teams que ha iniciado sesión para acceder a Microsoft Graph. Para obtener más información, consulte la característica sso a la aplicación de Teams.
Instale los paquetes necesarios
npm
.Ejecute el siguiente comando en la carpeta del proyecto
tabs
para instalar los paquetes necesariosnpm
:npm install @microsoft/mgt-react @microsoft/mgt-teamsfx-provider
Agregar un nuevo widget de Graph Toolkit: cree un nuevo archivo de widget en la carpeta del proyecto
src/views/widgets
, por ejemplo,GraphWidget.tsx
. En este widget, guiaremos a los usuarios para dar su consentimiento a nuestra aplicación para acceder a Microsoft Graph y, a continuación, mostrar la lista todo del usuario mediante Microsoft Graph Toolkit.El código siguiente es un ejemplo de uso del componente Todo del kit de herramientas de Microsoft Graph en el widget:
import { Providers, ProviderState, Todo } from "@microsoft/mgt-react"; import { TeamsFxProvider } from "@microsoft/mgt-teamsfx-provider"; import { loginAction } from "../../internal/login"; import { TeamsUserCredentialContext } from "../../internal/singletonContext"; import { BaseWidget } from "@microsoft/teamsfx-react"; interface IGraphWidgetState { needLogin: boolean; } export class GraphWidget extends Widget<any, IGraphWidgetState> { override body(): JSX.Element | undefined { return <div>{this.state.needLogin === false && <Todo />}</div>; } async componentDidMount() { super.componentDidMount(); // Initialize TeamsFx provider const provider = new TeamsFxProvider(TeamsUserCredentialContext.getInstance().getCredential(), [ "Tasks.ReadWrite", ]); Providers.globalProvider = provider; // Check if user is signed in if (await this.checkIsConsentNeeded()) { await loginAction(["Tasks.ReadWrite"]); } // Update signed in state Providers.globalProvider.setState(ProviderState.SignedIn); this.setState({ needLogin: false }); } /** * Check if user needs to consent * @returns true if user needs to consent */ async checkIsConsentNeeded() { let needConsent = false; try { await TeamsUserCredentialContext.getInstance().getCredential().getToken(["Tasks.ReadWrite"]); } catch (error) { needConsent = true; } return needConsent; } }
Puede usar componentes alternativos del kit de herramientas de Microsoft Graph en el widget. Para obtener más información, consulte Microsoft Graph Toolkit.
Agregue el widget al diseño del panel. Incluya el nuevo widget en el archivo del panel.
... export default class YourDashboard extends BaseDashboard<any, any> { ... override layout(): undefined | JSX.Element { return ( <> <GraphWiget /> </> ); } ... }
Ahora, inicie o actualice la aplicación de Teams, verá el nuevo widget con Microsoft Graph Toolkit.
llamada Graph API
Microsoft Graph API es una API web que puede usar para comunicarse con la nube de Microsoft y otros servicios. Las aplicaciones personalizadas pueden usar la API de Microsoft Graph para conectarse a los datos y usarla en aplicaciones personalizadas para mejorar la productividad de la organización.
Antes de implementar la lógica de llamadas de Graph API, es necesario habilitar el inicio de sesión único para el proyecto de panel. Para obtener más información, consulte Agregar inicio de sesión único a la aplicación Teams.
Para agregar una llamada Graph API:
- Llamar a Graph API desde el front-end (usar permisos delegados)
- Llamar a Graph API desde el back-end (usar permisos de aplicación)
Llamar a Graph API desde el front-end (usar permisos delegados)
Si desea llamar a un Graph API desde la pestaña front-end, siga estos pasos:
Para obtener el nombre del ámbito de permiso asociado a la Graph API que va a invocar, consulte Graph API.
Cree un cliente de Graph agregando el ámbito relacionado con el Graph API al que desea llamar.
let credential: TeamsUserCredential; credential = TeamsUserCredentialContext.getInstance().getCredential(); const graphClient: Client = createMicrosoftGraphClientWithCredential(credential, scope);
Llame al Graph API y analice la respuesta en un modelo determinado.
try { const graphApiResult = await graphClient.api("<GRAPH_API_PATH>").get(); // Parse the graphApiResult into a Model you defined, used by the front-end. } catch (e) {}
Llamar a Graph API desde el back-end (usar permisos de aplicación)
Si desea llamar a un Graph API desde el back-end, siga estos pasos:
- Permisos de aplicación de consentimiento
- Adición de una función de Azure
- Incorporación de la lógica en la función de Azure
- Llamada a la función de Azure desde el front-end
Permisos de aplicación de consentimiento
Para dar su consentimiento a los permisos de aplicación, siga estos pasos:
- Acceda a Portal Azure.
- Seleccione Microsoft Entra ID.
- Seleccione Registros de aplicaciones en el panel izquierdo.
- Seleccione la aplicación de panel.
- Seleccione Permisos de API en el panel izquierdo.
- Seleccione Agregar permiso.
- Seleccione Microsoft Graph.
- Seleccione Permisos de aplicación.
- Busque los permisos que necesita.
- Seleccione el botón Agregar permisos en la parte inferior.
- Seleccione ✔Conceder consentimiento de administrador.
- Seleccione el botón Sí para finalizar el consentimiento del administrador.
Adición de una función de Azure
En el panel izquierdo de la Visual Studio Code, vaya a Teams Toolkit>Adding features Azure Functions (Agregar características>Azure Functions y escriba el nombre de la función.
Para obtener más información sobre cómo agregar una función de Azure al proyecto, consulte Integración de Azure Functions con la aplicación de Teams.
Incorporación de la lógica en la función de Azure
En la index.ts
/index.ts
carpeta denominada Función de Azure, puede agregar la lógica que contiene el back-end Graph API llamadas con permisos de aplicación. Consulte el siguiente fragmento de código:
/**
* This function handles requests from teamsfx client.
* The HTTP request should contain an SSO token queried from Teams in the header.
* Before triggering this function, teamsfx binding would process the SSO token and generate teamsfx configuration.
*
* You should initializes the teamsfx SDK with the configuration and calls these APIs.
*
* The response contains multiple message blocks constructed into a JSON object, including:
* - An echo of the request body.
* - The display name encoded in the SSO token.
* - Current user's Microsoft 365 profile if the user has consented.
*
* @param {Context} context - The Azure Functions context object.
* @param {HttpRequest} req - The HTTP request.
* @param {teamsfxContext} TeamsfxContext - The context generated by teamsfx binding.
*/
export default async function run(
context: Context,
req: HttpRequest,
teamsfxContext: TeamsfxContext
): Promise<Response> {
context.log("HTTP trigger function processed a request.");
// Initialize response.
const res: Response = {
status: 200,
body: {},
};
// Your logic here.
return res;
}
Llamada a la función de Azure desde el front-end
Llame a la función de Azure por nombre de función. Consulte el siguiente fragmento de código para llamar a la función de Azure:
const functionName = process.env.REACT_APP_FUNC_NAME || "myFunc";
export let taskName: string;
export async function callFunction(params?: string) {
taskName = params || "";
const credential = TeamsUserCredentialContext.getInstance().getCredential();
if (!credential) {
throw new Error("TeamsFx SDK is not initialized.");
}
try {
const apiBaseUrl = process.env.REACT_APP_FUNC_ENDPOINT + "/api/";
const apiClient = createApiClient(
apiBaseUrl,
new BearerTokenAuthProvider(async () => (await credential.getToken(""))!.token)
);
const response = await apiClient.get(functionName);
return response.data;
} catch (err: unknown) {
...
}
}
Para más información, vea:
Inserción de Power BI en el panel
Para insertar Power BI en el panel, consulte Reacción del cliente de Power BI.
Guía paso a paso
Siga la guía paso a paso para crear un panel y aprenda a agregar un widget y Graph API llamada al panel.