Criar uma aplicação de separador do dashboard
Um dashboard é uma ferramenta para monitorizar, analisar e apresentar dados para obter informações sobre uma organização ou um processo específico. Os dashboards no Teams permitem-lhe monitorizar e ver métricas importantes.
O modelo de separador do dashboard do Teams Toolkit permite-lhe começar a integrar uma tela com vários cartões que fornecem uma descrição geral dos conteúdos no Teams. Você pode:
- Utilize widgets para apresentar conteúdos de aplicações e serviços no separador do dashboard.
- Integre a sua aplicação com o Graph API para visualizar detalhes sobre a implementação dos dados selecionados.
- Crie dashboards personalizáveis que permitam à sua empresa definir objetivos específicos que o ajudam a controlar as informações que precisa de ver em várias áreas e em vários departamentos.
A sua equipa pode obter as atualizações mais recentes de diferentes origens no Teams através da aplicação de separador dashboard do Teams. Utilize aplicações de separador do dashboard para ligar várias métricas, origens de dados, APIs e serviços. As aplicações de separador do dashboard ajudam a sua empresa a extrair informações relevantes das origens e a apresentá-la aos utilizadores. Para obter mais informações sobre como criar uma aplicação de separador de dashboard, veja Guia passo a passo.
Adicionar um novo dashboard
Depois de criar uma aplicação de separador do dashboard, pode adicionar um novo dashboard.
Para adicionar um novo dashboard, siga estes passos:
- Criar uma classe de dashboard
- Substituir métodos para personalizar a aplicação de separador do dashboard
- Adicionar uma rota para a nova aplicação de separador do dashboard
- Modificar manifesto para adicionar uma nova aplicação de separador do dashboard
Criar uma classe de dashboard
Crie um ficheiro com a extensão para o .tsx
dashboard no src/dashboards
diretório, por exemplo, YourDashboard.tsx
. Em seguida, crie uma classe que expanda o BaseDashboard class from
@microsoft/teamsfx-react.
//YourDashboard.tsx
import { BaseDashboard } from "@microsoft/teamsfx-react";
export default class YourDashboard extends BaseDashboard<any, any> {}
Observação
Todos os métodos são opcionais. Se não substituir nenhum método, é utilizado o esquema de dashboard predefinido.
Substituir métodos para personalizar a aplicação de separador do dashboard
A BaseDashboard
classe fornece poucos métodos que pode substituir para personalizar o esquema do dashboard. A tabela seguinte lista os métodos que pode substituir:
Métodos | Function |
---|---|
styling() |
Personalize o estilo do dashboard. |
layout() |
Definir esquema de widgets. |
O código seguinte é um exemplo para personalizar o esquema do dashboard:
.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<any, any> {
override styling(): string {
return "your-dashboard-layout";
}
override layout(): JSX.Element | undefined {
return (
<>
<ListWidget />
<ChartWidget />
</>
);
}
}
Adicionar uma rota para a nova aplicação de separador do dashboard
Tem de ligar o widget a um ficheiro de origem de dados. O widget recolhe os dados apresentados no dashboard a partir do ficheiro de origem.
Abra o src/App.tsx
ficheiro e adicione uma rota para o novo dashboard. Veja um exemplo:
import YourDashboard from "./dashboards/YourDashboard";
export default function App() {
...
<Route path="/yourdashboard" element={<YourDashboard />} />
...
}
Modificar manifesto para adicionar uma nova aplicação de separador do dashboard
Abra o appPackage/manifest.json
ficheiro e adicione um novo separador do dashboard em staticTabs
. Para obter mais informações, consulte o manifesto do aplicativo. Veja um exemplo:
{
"entityId": "index1",
"name": "Your Dashboard",
"contentUrl": "${{TAB_ENDPOINT}}/index.html#/yourdashboard",
"websiteUrl": "${{TAB_ENDPOINT}}/index.html#/yourdashboard",
"scopes": ["personal"]
}
Personalizar o esquema do dashboard
O TeamsFx fornece métodos convenientes para definir e modificar o esquema do dashboard. Seguem-se os métodos:
Três widgets seguidos com a altura de 350 px ocupando 20 por cento, 60 por cento e 20 por cento da largura, respetivamente.
.customize-class-name { grid-template-rows: 350px; grid-template-columns: 2fr 6fr 2fr; }
export default class SampleDashboard extends BaseDashboard<any, any> { override styling(): string { return "customize-class-name"; } override layout(): JSX.Element | undefined { return ( <> <ListWidget /> <ChartWidget /> <NewsWidget /> </> ); } }
Dois widgets numa linha com uma largura de 600 px e 1100 px. A altura da primeira linha é a altura máxima do conteúdo e a altura da segunda linha é de 400 px.
.customize-class-name { grid-template-rows: max-content 400px; grid-template-columns: 600px 1100px; }
export default class SampleDashboard extends Dashboard { override styling(): string { return "customize-class-name"; } override layout(): JSX.Element | undefined { return ( <> <ListWidget /> <ChartWidget /> <NewsWidget /> </> ); } }
Disponha dois widgets numa coluna.
.one-column { display: grid; gap: 20px; grid-template-rows: 1fr 1fr; }
export default class SampleDashboard extends BaseDashboard<any, any> { override layout(): JSX.Element | undefined { return ( <> <NewsWidget /> <div className="one-column"> <ListWidget /> <ChartWidget /> </div> </> ); } }
Abstração da aplicação do separador Dashboard
Para ajustar o esquema do dashboard, o TeamsFx fornece uma BaseDashboard
classe para os programadores implementarem um dashboard.
O código seguinte é um exemplo de uma BaseDashboard
classe:
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;
}
}
Na classe , o BaseDashboard
TeamsFx fornece esquemas básicos com métodos personalizáveis. O dashboard continua a ser um componente de reação e o TeamsFx fornece implementações básicas de funções com base no ciclo de vida dos componentes do React, tais como:
- Implementar uma lógica de composição básica com base no esquema de grelha.
- Adicionar um observador para se adaptar automaticamente aos dispositivos móveis.
Seguem-se os métodos personalizáveis a substituir:
Métodos | Função | Recomendamos que substitua |
---|---|---|
constructor() |
Inicializa o estado e as variáveis do dashboard. | Não |
componentDidMount() |
Invoca após a montagem de um componente. | Não |
componentWillUnmount() |
Invoca quando um componente é desmontado. | Não |
render() |
Invoca quando existe uma atualização. O esquema predefinido do dashboard é definido neste método. | Não |
layout |
Define o esquema do widget no dashboard. Pode substituir este método. | Sim |
styling() |
Para personalizar o estilo do dashboard. Pode substituir este método. | Sim |
Utilizar um widget no dashboard
Os widgets apresentam informações configuráveis e gráficos nos dashboards. Aparecem no quadro de widget onde pode afixar, remover, dispor, redimensionar e personalizar widgets para refletir os seus interesses. O seu quadro de widget está otimizado para mostrar widgets relevantes e conteúdos personalizados com base na sua utilização.
Personalizar o widget
Pode personalizar o widget ao substituir os seguintes métodos na BaseWidget
classe :
Substitua
header()
,body()
efooter()
para personalizar o 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> ); } }
Substitua
body()
efooter()
personalize o 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> ); } }
Substitua
body()
para personalizar o 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 um carregador de dados
Se quiser incluir um carregador de dados no widget antes de o widget ser carregado, pode adicionar uma propriedade ao estado do widget para indicar que o carregador de dados é loading()
. Pode utilizar esta propriedade para mostrar um indicador de carregamento ao utilizador.
Exemplo:
override loading(): JSX.Element | undefined {
return (
<div className="loading">
<Spinner label="Loading..." labelPosition="below" />
</div>
);
}
Agora, o controlo giratório de carregamento é apresentado enquanto os dados estão a ser carregados. Quando os dados são carregados, o controlo giratório de carregamento é ocultado e os dados da lista e o botão de rodapé são apresentados.
Processar o estado vazio
Pode apresentar um conteúdo específico no seu widget quando os dados estiverem vazios. Para tal, tem de modificar o body
método no ficheiro widget para adotar diferentes estados dos dados.
O exemplo seguinte mostra como apresentar uma imagem vazia quando os dados de ListWidget estão vazios.
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>
);
}
Pode utilizar uma abordagem semelhante para remover o conteúdo do rodapé do widget quando os dados estão vazios.
override footer(): JSX.Element | undefined {
let hasData = this.state.data && this.state.data.length > 0;
if (hasData) {
return <Button>...</Button>;
}
}
Quando os dados estão vazios, o widget de lista é apresentado da seguinte forma:
Atualizar dados conforme agendado
O exemplo seguinte mostra como apresentar dados em tempo real num widget. O widget apresenta a hora e as atualizações atuais.
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);
}
}
Pode modificar setInterval
o método para chamar a sua própria função para atualizar dados como este setInterval(() => yourGetDataFunction(), 1000)
.
Abstração de widget
Para simplificar o desenvolvimento de um widget, o SDK teamsFx fornece uma BaseWidget
classe para os programadores herdarem para implementar um widget que satisfaz as suas necessidades sem prestar muita atenção para implementar o esquema de widget.
O código seguinte é um exemplo da classe 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 {};
}
}
Seguem-se os métodos recomendados para substituir:
Métodos | Função | Recomendamos que substitua |
---|---|---|
constructor() |
Invoca a inicial this.state e chama o construtor da super classe React.Component . |
Não |
componentDidMount() |
Invoca depois de um componente ser montado e atribui um valor à data propriedade do estado ao chamar o getData() método . |
Não |
render() |
Invoca sempre que existir uma atualização. O esquema predefinido do dashboard é definido neste método. | Não |
getData() |
Invoca os dados necessários para o widget. O valor devolvido por este método está definido como this.state.data . |
Sim |
header() |
Invoca o aspeto do cabeçalho do widget. Pode optar por substituir este método para personalizar um widget ou não, caso contrário, o widget não terá um cabeçalho. | Sim |
body() |
Invoca o aspeto do corpo do widget. Pode optar por substituir este método para personalizar um widget ou não, caso contrário, o widget não terá um corpo. | Sim |
footer() |
Invoca o aspeto do rodapé do widget. Pode optar por substituir este método para personalizar um widget ou não, caso contrário, o widget não terá um rodapé. | Sim |
loading() |
Invoca quando o widget está em processo de obtenção de dados. Se for necessário um indicador de carregamento, o método pode devolver um JSX.Element que contém os componentes necessários para compor o indicador de carregamento. |
Sim |
style() |
Invoca um objeto que define os nomes de classe para as diferentes partes do widget. | Sim |
Microsoft Graph Toolkit como conteúdo de widget
O Microsoft Graph Toolkit é um conjunto de componentes Web renováveis e agnósticos, que ajudam a aceder e trabalhar com o Microsoft Graph. Pode utilizar o Microsoft Graph Toolkit com qualquer arquitetura Web ou sem uma arquitetura.
Para utilizar o Microsoft Graph Toolkit como conteúdo do widget, siga estes passos:
Adicionar a funcionalidade SSO à sua aplicação Teams: o Microsoft Teams fornece uma função de início de sessão único (SSO) para uma aplicação obter o token de utilizador com sessão iniciada no Teams para aceder ao Microsoft Graph. Para obter mais informações, consulte a funcionalidade SSO para a sua aplicação Teams.
Instale os
npm
pacotes necessários.Execute o seguinte comando na pasta do projeto
tabs
para instalar os pacotes necessáriosnpm
:npm install @microsoft/mgt-react @microsoft/mgt-teamsfx-provider
Adicionar um novo widget Graph Toolkit: crie um novo ficheiro widget na pasta do projeto
src/views/widgets
, por exemplo,GraphWidget.tsx
. Neste widget, iremos orientar os utilizadores para dar consentimento à nossa aplicação para aceder ao Microsoft Graph e, em seguida, mostrar a lista Todo do utilizador com o Microsoft Graph Toolkit.O código seguinte é um exemplo da utilização do componente Todo do Microsoft Graph Toolkit no 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; } }
Pode utilizar componentes alternativos do Microsoft Graph Toolkit no seu widget. Para obter mais informações, consulte Microsoft Graph Toolkit.
Adicione o widget ao esquema do dashboard. Inclua o novo widget no seu ficheiro de dashboard.
... export default class YourDashboard extends BaseDashboard<any, any> { ... override layout(): undefined | JSX.Element { return ( <> <GraphWiget /> </> ); } ... }
Agora, inicie ou atualize a sua aplicação Teams, verá o novo widget com o Microsoft Graph Toolkit.
Chamada à Graph API
A Microsoft Graph API é uma API Web que pode utilizar para comunicar com a Microsoft Cloud e outros serviços. Aplicativos personalizados podem usar a API do Microsoft Graph para se conectar aos dados e usá-los em aplicativos personalizados para aprimorar a produtividade organizacional.
Antes de implementar a lógica de chamadas da Graph API, é necessário ativar o SSO para o projeto de dashboard. Para obter mais informações, consulte Adicionar início de sessão único à aplicação Teams.
Para adicionar uma chamada à Graph API:
- Chamar a Graph API a partir do front-end (utilizar permissões delegadas)
- Chamar a Graph API a partir do back-end (utilizar permissões de aplicação)
Chamar a Graph API a partir do front-end (utilizar permissões delegadas)
Se quiser chamar uma Graph API a partir do separador de front-end, siga estes passos:
Para obter o nome do âmbito de permissão associado à Graph API que pretende invocar, veja Graph API.
Crie um cliente do Graph ao adicionar o âmbito relacionado com a Graph API que pretende chamar.
let credential: TeamsUserCredential; credential = TeamsUserCredentialContext.getInstance().getCredential(); const graphClient: Client = createMicrosoftGraphClientWithCredential(credential, scope);
Chame a Graph API e analise a resposta num determinado modelo.
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) {}
Chamar a Graph API a partir do back-end (utilizar permissões de aplicação)
Se quiser chamar uma Graph API a partir do back-end, siga estes passos:
- Consent application permissions (Consent application permissions)
- Adicionar uma função do Azure
- Adicionar a lógica na função do Azure
- Chamar a função do Azure a partir do front-end
Consent application permissions (Consent application permissions)
Para dar consentimento às permissões da aplicação, siga estes passos:
- Acesse o portal do Azure.
- Selecione Microsoft Entra ID.
- Selecione Registos de aplicações no painel esquerdo.
- Selecione a sua aplicação de dashboard.
- Selecione Permissões de API no painel esquerdo.
- Selecione Adicionar permissão.
- Selecione Microsoft Graph.
- Selecione Permissões de aplicativos.
- Localize as permissões de que precisa.
- Selecione o botão Adicionar permissões na parte inferior.
- Selecione ✔Conceder consentimento do administrador.
- Selecione o botão Sim para concluir o consentimento do administrador.
Adicionar uma função do Azure
No painel esquerdo do Visual Studio Code, aceda a Toolkit> do TeamsAdicionar funcionalidades>funções do Azure e introduza o nome da função.
Para obter mais informações sobre como adicionar uma Função do Azure ao seu projeto, veja Integrar as Funções do Azure com a sua aplicação Teams.
Adicionar a lógica na função do Azure
Na pasta com o nome Função do Azure, pode adicionar a index.ts
/index.ts
sua lógica que contém chamadas de Graph API de back-end com permissões de aplicação. Veja o seguinte 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;
}
Chamar a função do Azure a partir do front-end
Chame a função do Azure pelo nome da função. Veja o fragmento de código seguinte para chamar a função do 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 saber mais, confira:
Incorporar o Power BI no dashboard
Para incorporar o Power BI no dashboard, veja Reação do cliente do Power BI.
Guias passo a passo
Siga o guia passo a passo para criar um dashboard e saiba como adicionar um widget e uma chamada à Graph API ao dashboard.