生成仪表板选项卡应用
仪表板是一种跟踪、分析和显示数据以深入了解组织或特定流程的工具。 Teams 中的仪表板允许监视和查看重要指标。
Teams 工具包中的“仪表板”选项卡模板允许你开始将画布与多个卡片集成,这些卡片提供 Teams 中内容的概述。 可以执行下列操作:
- 使用小组件在“仪表板”选项卡中显示应用和服务中的内容。
- 将应用与图形 API集成,以可视化有关所选数据实现的详细信息。
- 创建可自定义的仪表板,使你的企业能够设置特定目标,帮助你跟踪你需要在多个领域和跨部门查看的信息。
你的团队可以使用 Teams 仪表板 选项卡应用从 Teams 中的不同源获取最新更新。 使用仪表板选项卡应用来连接许多指标、数据源、API 和服务。 仪表板选项卡应用可帮助企业从源中提取相关信息,并将其呈现给用户。 有关创建仪表板选项卡应用的详细信息,请参阅分步指南。
添加新仪表板
创建仪表板选项卡应用后,可以添加新仪表板。
若要添加新仪表板,请执行以下步骤:
创建仪表板类
在 目录中创建扩展名.tsx
为 仪表板 src/dashboards
的文件,例如 YourDashboard.tsx
。 然后,创建一个扩展 的类 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 { }
注意
所有方法都是可选的。 如果不重写任何方法,则使用默认仪表板布局。
重写方法以自定义仪表板选项卡应用
类BaseDashboard
提供了一些可替代方法来自定义仪表板布局。 下表列出了可以替代的方法:
方法 | 函数 |
---|---|
styling() |
自定义仪表板的样式。 |
layout() |
定义小组件布局。 |
以下代码是自定义仪表板布局的示例:
.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 />
</>
);
}
}
为新的仪表板选项卡应用添加路由
必须将小组件链接到数据源文件。 小组件从源文件中选取仪表板中显示的数据。
打开 文件并src/App.tsx
添加新仪表板的路由。 下面是一个示例:
import YourDashboard from "./dashboards/YourDashboard";
export default function App() {
...
<Route path="/yourdashboard" element={<yourdashboard />} />
...
}
修改清单以添加新仪表板选项卡应用
打开 文件,appPackage/manifest.json
并在 下staticTabs
添加新的“仪表板”选项卡。 有关详细信息,请参阅 应用清单。 下面是一个示例:
{
"entityId": "index1",
"name": "Your Dashboard",
"contentUrl": "${{TAB_ENDPOINT}}/index.html#/yourdashboard",
"websiteUrl": "${{TAB_ENDPOINT}}/index.html#/yourdashboard",
"scopes": ["personal"]
}
自定义仪表板布局
TeamsFx 提供了用于定义和修改仪表板布局的便捷方法。 以下是方法:
一行中的三个小组件,高度为 350 像素,分别占宽度的 20%、60% 和 20%。
.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 /> </> ); } }
一行中的两个小组件,宽度为 600 像素和 1100 像素。 第一行的高度是其内容的最大高度,第二行的高度为 400 像素。
.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 /> </> ); } }
在一列中排列两个小组件。
.one-column { display: grid; gap: 20px; grid-template-rows: 1fr 1fr; }
export default class SampleDashboard extends BaseDashboard { layout() { return ( <> <ListWidget /> <ChartWidget /> </> ); } }
仪表板选项卡应用抽象
为了调整仪表板的布局,TeamsFx 提供了一个BaseDashboard
类,供开发人员实现仪表板。
以下代码是 类的示例 BaseDashboard
:
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;
}
}
在 类中 BaseDashboard
,TeamsFx 提供具有可自定义方法的基本布局。 仪表板仍然是一个响应组件,TeamsFx 根据响应组件的生命周期提供函数的基本实现,例如:
- 基于网格布局实现基本呈现逻辑。
- 添加观察程序以自动适应移动设备。
下面是要替代的可自定义方法:
方法 | 函数 | 建议重写 |
---|---|---|
constructor() |
初始化仪表板状态和变量。 | 否 |
componentDidMount() |
在装载组件后调用。 | 否 |
componentWillUnmount() |
卸载组件时调用 。 | 否 |
render() |
发生更新时调用 。 此方法中定义了仪表板默认布局。 | 否 |
layout |
定义仪表板中小组件的布局。 可以重写此方法。 | 是 |
styling() |
自定义仪表板的样式。 可以重写此方法。 | 是 |
在仪表板中使用小组件
小组件在仪表板上显示可配置的信息和图表。 它们显示在小组件板上,你可以在其中固定、取消固定、排列、调整小组件大小和自定义小组件以反映你的兴趣。 小组件板经过优化,可根据使用情况显示相关小组件和个性化内容。
自定义小组件
可以通过重写 类中的 BaseWidget
以下方法来自定义小组件:
重写
header()
、body()
和footer()
以自定义小组件。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> ); } }
重写
body()
和footer()
以自定义小组件。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> ); } }
重写
body()
以自定义小组件。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> ); } }
包括数据加载程序
如果要在加载小组件之前将数据加载程序包含在小组件中,可以将属性添加到小组件的状态,以指示数据加载程序为 loading()
。 可以使用此属性向用户显示加载指示器。
示例:
override loading(): JSX.Element | undefined {
return (
<div className="loading">
<Spinner label="Loading..." labelPosition="below" />
</div>
);
}
现在,加载数据时会显示加载微调器。 加载数据时,加载微调器处于隐藏状态,并显示列表数据和页脚按钮。
处理空状态
当数据为空时,可以在小组件中显示特定内容。 为此,需要修改 body
小组件文件中的 方法,以采用数据的不同状态。
以下示例演示如何在 ListWidget 的数据为空时显示空图像。
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>
);
}
当数据为空时,可以使用类似的方法来删除小组件的页脚内容。
override footer(): JSX.Element | undefined {
let hasData = this.state.data && this.state.data.length > 0;
if (hasData) {
return <Button>...</Button>;
}
}
当数据为空时,列表小组件如下所示:
按计划刷新数据
以下示例演示如何在小组件中显示实时数据。 小组件显示当前时间和更新。
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);
}
}
可以修改 setInterval
方法以调用自己的函数来刷新数据,如下所示 setInterval(() => yourGetDataFunction(), 1000)
。
小组件抽象
为了简化小组件的开发,TeamsFx SDK 提供了一个 BaseWidget
类,供开发人员继承以实现满足其需求的小组件,而无需过多关注实现小组件布局。
以下代码是 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 {};
}
}
下面是替代的建议方法:
方法 | 函数 | 建议重写 |
---|---|---|
constructor() |
调用初始 this.state 并调用超级类 React.Component 的构造函数。 |
否 |
componentDidMount() |
在装载组件后调用 ,并通过调用 getData() 方法将值分配给data 状态的 属性。 |
否 |
render() |
每当有更新时调用 。 此方法中定义了仪表板默认布局。 | 否 |
getData() |
调用小组件所需的数据。 此方法返回的值设置为 this.state.data 。 |
是 |
header() |
调用小组件标头的外观。 可以选择重写此方法以自定义小组件,否则小组件将不具有标头。 | 是 |
body() |
调用小组件正文的外观。 可以选择重写此方法以自定义小组件,否则小组件将不具有正文。 | 是 |
footer() |
调用小组件页脚的外观。 可以选择重写此方法以自定义小组件,否则小组件将没有页脚。 | 是 |
loading() |
当小组件正在提取数据时调用 。 如果需要加载指示器,方法可以返回一个 JSX.Element ,其中包含呈现加载指示器所需的组件。 |
是 |
style() |
调用一个 对象,该对象定义小组件的不同部分的类名称。 | 是 |
Microsoft Graph 工具包作为小组件内容
Microsoft Graph 工具包是一组可更新的、与框架无关的 Web 组件,可帮助访问和使用 Microsoft Graph。 可以将 Microsoft Graph 工具包与任何 Web 框架一起使用,也可以不使用框架。
若要将 Microsoft Graph 工具包用作小组件内容,请执行以下步骤:
向 Teams 应用添加 SSO 功能:Microsoft Teams 提供单一登录 (SSO) 功能,使应用获取已登录的 Teams 用户令牌以访问 Microsoft Graph。 有关详细信息,请参阅 Teams 应用的 SSO 功能。
安装所需的
npm
包。在项目
tabs
文件夹中运行以下命令以安装所需的npm
包:npm install @microsoft/mgt-react @microsoft/mgt-teamsfx-provider
添加新的 Graph 工具包小组件:在项目
src/views/widgets
文件夹中创建新的小组件文件,GraphWidget.tsx
例如 。 在此小组件中,我们将引导用户同意应用访问 Microsoft Graph,然后使用 Microsoft Graph 工具包显示用户的待办事项列表。以下代码是在小组件中使用 Microsoft Graph 工具包中的 Todo 组件的示例:
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; } }
可以在小组件中使用替代Microsoft Graph 工具包组件。 有关详细信息,请参阅 Microsoft Graph 工具包。
将小组件添加到仪表板布局。 在仪表板文件中包含新小组件。
... export default class YourDashboard extends BaseDashboard<any, any> { ... override layout(): undefined | JSX.Element { return ( <> <GraphWiget /> </> ); } ... }
现在,启动或刷新 Teams 应用,你将看到使用 Microsoft Graph 工具包的新小组件。
图形 API调用
Microsoft图形 API是一个 Web API,可用于与Microsoft云和其他服务进行通信。 自定义应用程序可以使用 Microsoft Graph API 连接到数据,并在自定义应用程序中使用它来增强组织工作效率。
在实现图形 API调用逻辑之前,需要为仪表板项目启用 SSO。 有关详细信息,请参阅 向 Teams 应用添加单一登录。
添加图形 API调用:
从前端调用图形 API (使用委托的权限)
如果要从前端选项卡调用图形 API,请执行以下步骤:
若要获取与要调用的图形 API关联的权限范围的名称,请参阅 图形 API。
通过添加与要调用图形 API相关的范围来创建 Graph 客户端。
let credential: TeamsUserCredential; credential = TeamsUserCredentialContext.getInstance().getCredential(); const graphClient: Client = createMicrosoftGraphClientWithCredential(credential, scope);
调用图形 API并将响应分析为特定模型。
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) {}
从后端调用图形 API (使用应用程序权限)
如果要从后端调用图形 API,请执行以下步骤:
同意应用程序权限
若要同意应用程序权限,请执行以下步骤:
- 转到Azure 门户。
- 选择“Microsoft Entra ID”。
- 在左窗格中选择“应用注册”。
- 选择仪表板应用。
- 在左窗格中选择“ API 权限 ”。
- 选择“ 添加权限”。
- 选择 Microsoft Graph。
- 选择“应用程序权限”。
- 查找所需的权限。
- 选择底部的 “添加权限 ”按钮。
- 选择“ ✔授予管理员同意”。
- 选择“ 是 ”按钮以完成管理员同意。
添加 Azure 函数
在Visual Studio Code的左窗格中,转到 Teams 工具包>添加功能>Azure Functions并输入函数名称。
有关如何将 Azure 函数添加到项目的详细信息,请参阅将 Azure Functions 与 Teams 应用集成。
在 Azure 函数中添加逻辑
index.ts
/
index.ts
在名为 Azure Function 的文件夹下,可以添加包含后端图形 API调用应用程序权限的逻辑。 请参阅以下代码片段:
/**
* 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;
}
从前端调用 Azure 函数
按函数名称调用 Azure 函数。 请参阅以下代码片段来调用 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) {
...
}
}
有关更多信息,请参阅:
嵌入 Power BI 以仪表板
若要将 Power BI 嵌入到仪表板,请参阅 Power BI 客户端响应。
分步指南
按照分步指南生成仪表板,并了解如何向仪表板添加小组件和图形 API调用。