Skip to content

Commit

Permalink
Dashboard insights flyout with dashboard views (#187993)
Browse files Browse the repository at this point in the history
## Summary

close #183687

## Feature

- Implement dashboard view stats UI on top of usage counter that counts
dashboard views for last 90 day and shows weekly histogram.
- (Even if there is not a lot of data, we still show it as a weekly
histogram, so it can be pretty empty intially)

![Screenshot 2024-08-15 at 13 00
11](https://github.com/user-attachments/assets/adeabf78-e3d3-4cfa-adc3-76a32ede595b)


## Implementation

### Server side
Dashboard plugin registers new routes to increase the view count and get
stats. Routes are protected for users with dashboard access only. The
implementation is located in
`@kbn/content-management-content-insights-server` and internally uses
usage counters. The retention is 90 days, so we can only show stats for
last 90 days.

 ### Client side

- Dashboard uses the client from
`@kbn/content-management-content-insights-public` to increase the view
count every time a user opens a dashboard.
- TableListView opens the flyout from
`@kbn/content-management-content-insights-public`to display the stats

## How to test

- For new views just open a dashboard and check that view stat is
increased
- For old views you can populate the usage counters with historic data.
I used the following script:
https://gist.github.com/Dosant/425042fcf75d5e40e5a46374f6234a54
  • Loading branch information
Dosant authored Aug 21, 2024
1 parent 6db6a8d commit 64e1116
Show file tree
Hide file tree
Showing 61 changed files with 1,172 additions and 175 deletions.
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ packages/kbn-config-mocks @elastic/kibana-core
packages/kbn-config-schema @elastic/kibana-core
src/plugins/console @elastic/kibana-management
packages/content-management/content_editor @elastic/appex-sharedux
packages/content-management/content_insights/content_insights_public @elastic/appex-sharedux
packages/content-management/content_insights/content_insights_server @elastic/appex-sharedux
examples/content_management_examples @elastic/appex-sharedux
packages/content-management/favorites/favorites_public @elastic/appex-sharedux
packages/content-management/favorites/favorites_server @elastic/appex-sharedux
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@
"@kbn/config-schema": "link:packages/kbn-config-schema",
"@kbn/console-plugin": "link:src/plugins/console",
"@kbn/content-management-content-editor": "link:packages/content-management/content_editor",
"@kbn/content-management-content-insights-public": "link:packages/content-management/content_insights/content_insights_public",
"@kbn/content-management-content-insights-server": "link:packages/content-management/content_insights/content_insights_server",
"@kbn/content-management-examples-plugin": "link:examples/content_management_examples",
"@kbn/content-management-favorites-public": "link:packages/content-management/favorites/favorites_public",
"@kbn/content-management-favorites-server": "link:packages/content-management/favorites/favorites_server",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import type { Item } from '../types';
import { MetadataForm } from './metadata_form';
import { useMetadataForm } from './use_metadata_form';
import type { CustomValidators } from './use_metadata_form';
import { ActivityView } from './activity_view';

const getI18nTexts = ({ entityName }: { entityName: string }) => ({
saveButtonLabel: i18n.translate('contentManagement.contentEditor.saveButtonLabel', {
Expand Down Expand Up @@ -56,7 +55,7 @@ export interface Props {
}) => Promise<void>;
customValidators?: CustomValidators;
onCancel: () => void;
showActivityView?: boolean;
appendRows?: React.ReactNode;
}

const capitalize = (str: string) => `${str.charAt(0).toLocaleUpperCase()}${str.substring(1)}`;
Expand All @@ -70,7 +69,7 @@ export const ContentEditorFlyoutContent: FC<Props> = ({
onSave,
onCancel,
customValidators,
showActivityView,
appendRows,
}) => {
const { euiTheme } = useEuiTheme();
const [isSubmitting, setIsSubmitting] = useState(false);
Expand Down Expand Up @@ -151,7 +150,7 @@ export const ContentEditorFlyoutContent: FC<Props> = ({
TagList={TagList}
TagSelector={TagSelector}
>
{showActivityView && <ActivityView item={item} />}
{appendRows}
</MetadataForm>
</EuiFlyoutBody>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type CommonProps = Pick<
| 'onCancel'
| 'entityName'
| 'customValidators'
| 'showActivityView'
| 'appendRows'
>;

export type Props = CommonProps;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,30 @@
* Side Public License, v 1.
*/

import React, { useState, useCallback, useEffect } from 'react';
import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui';
import React from 'react';
import { EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader } from '@elastic/eui';
import type { Props } from './editor_flyout_content_container';

export const ContentEditorLoader: React.FC<Props> = (props) => {
const [Editor, setEditor] = useState<React.ComponentType<Props> | null>(null);

const loadEditor = useCallback(async () => {
const { ContentEditorFlyoutContentContainer } = await import(
'./editor_flyout_content_container'
);
setEditor(() => ContentEditorFlyoutContentContainer);
}, []);
const ContentEditorFlyoutContentContainer = React.lazy(() =>
import('./editor_flyout_content_container').then(
({ ContentEditorFlyoutContentContainer: _ContentEditorFlyoutContentContainer }) => ({
default: _ContentEditorFlyoutContentContainer,
})
)
);

useEffect(() => {
// On mount: load the editor asynchronously
loadEditor();
}, [loadEditor]);

return Editor ? (
<Editor {...props} />
) : (
<>
<EuiFlyoutHeader />
<EuiFlyoutBody />
<EuiFlyoutFooter />
</>
export const ContentEditorLoader: React.FC<Props> = (props) => {
return (
<React.Suspense
fallback={
<>
<EuiFlyoutHeader />
<EuiFlyoutBody />
<EuiFlyoutFooter />
</>
}
>
<ContentEditorFlyoutContentContainer {...props} />
</React.Suspense>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -262,22 +262,5 @@ describe('<ContentEditorFlyoutContent />', () => {
tags: ['id-3', 'id-4'], // New selection
});
});

test('should render activity view', async () => {
await act(async () => {
testBed = await setup({ showActivityView: true });
});
const { find, component } = testBed!;

expect(find('activityView').exists()).toBe(true);
expect(find('activityView.createdByCard').exists()).toBe(true);
expect(find('activityView.updatedByCard').exists()).toBe(false);

testBed.setProps({
item: { ...savedObjectItem, updatedAt: '2021-01-01T00:00:00Z' },
});
component.update();
expect(find('activityView.updatedByCard').exists()).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type OpenContentEditorParams = Pick<
| 'readonlyReason'
| 'entityName'
| 'customValidators'
| 'showActivityView'
| 'appendRows'
>;

export function useOpenContentEditor() {
Expand Down
1 change: 0 additions & 1 deletion packages/content-management/content_editor/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"@kbn/test-jest-helpers",
"@kbn/react-kibana-mount",
"@kbn/content-management-user-profiles",
"@kbn/user-profile-components"
],
"exclude": [
"target/**/*"
Expand Down
64 changes: 64 additions & 0 deletions packages/content-management/content_insights/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
id: sharedUX/ContentInsights
slug: /shared-ux/content-insights
title: Content Insights
description: A set of Content Management services and component to provide insights on the content of Kibana.
tags: ['shared-ux', 'component']
date: 2024-08-06
---

## Description

The Content Insights is a set of Content Management services and components to provide insights on the content of Kibana.
Currently, it allows to track the usage of your content and display the stats of it.

- The service can count the following events:
- `viewed`
- It provides the api for registering the routes to increase the count and to get the stats.
- It provides the client to increase the count and to get the stats.
- It provides a flyout and a component to display the stats as a total count and a weekly chart.
- Internally it uses the usage collection plugin to store and search the data.

## API

// server side

```ts
import { registerContentInsights } from '@kbn/content-management-content-insights-server';

if (plugins.usageCollection) {
// Registers routes for tracking and fetching dashboard views
registerContentInsights(
{
usageCollection: plugins.usageCollection,
http: core.http,
getStartServices: () =>
core.getStartServices().then(([_, start]) => ({
usageCollection: start.usageCollection!,
})),
},
{
domainId: 'dashboard',
// makes sure that only users with read/all access to dashboard app can access the routes
routeTags: ['access:dashboardUsageStats'],
}
);
}
```

// client side

```ts
import { ContentInsightsClient } from '@kbn/content-management-content-insights-public';

const contentInsightsClient = new ContentInsightsClient(
{ http: params.coreStart.http },
{ domainId: 'dashboard' }
);

contentInsightsClient.track(dashboardId, 'viewed');

// wrap component in `ContentInsightsProvider` and use the hook to open an insights flyout
const openInsightsFlyout = useOpenInsightsFlyout();
openInsightsFlyout({ item });
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @kbn/content-management-content-insights-public

Refer to [README](../README.mdx)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export {
ContentInsightsProvider,
type ContentInsightsServices,
useServices as useContentInsightsServices,
} from './src/services';

export {
type ContentInsightsClientPublic,
ContentInsightsClient,
type ContentInsightsEventTypes,
} from './src/client';

export { ActivityView, ViewsStats, type ActivityViewProps } from './src/components';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/packages/content-management/content_insights/content_insights_public'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "shared-browser",
"id": "@kbn/content-management-content-insights-public",
"owner": "@elastic/appex-sharedux"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@kbn/content-management-content-insights-public",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { HttpStart } from '@kbn/core-http-browser';
import type {
ContentInsightsStats,
ContentInsightsStatsResponse,
} from '@kbn/content-management-content-insights-server';

export type ContentInsightsEventTypes = 'viewed';

/**
* Public interface of the Content Management Insights service.
*/
export interface ContentInsightsClientPublic {
track(id: string, eventType: ContentInsightsEventTypes): void;
getStats(id: string, eventType: ContentInsightsEventTypes): Promise<ContentInsightsStats>;
}

/**
* Client for the Content Management Insights service.
*/
export class ContentInsightsClient implements ContentInsightsClientPublic {
constructor(
private readonly deps: { http: HttpStart },
private readonly config: { domainId: string }
) {}

track(id: string, eventType: ContentInsightsEventTypes) {
this.deps.http
.post(`/internal/content_management/insights/${this.config.domainId}/${id}/${eventType}`)
.catch((e) => {
// eslint-disable-next-line no-console
console.warn(`Could not track ${eventType} event for ${id}`, e);
});
}

async getStats(id: string, eventType: ContentInsightsEventTypes) {
return this.deps.http
.get<ContentInsightsStatsResponse>(
`/internal/content_management/insights/${this.config.domainId}/${id}/${eventType}/stats`
)
.then((response) => response.result);
}
}
Loading

0 comments on commit 64e1116

Please sign in to comment.