mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-28 02:17:53 +00:00
feat(ui): Partial support for Chart usage (#5473)
This commit is contained in:
parent
935b423452
commit
f8697ba54f
@ -82,6 +82,7 @@ import com.linkedin.datahub.graphql.resolvers.auth.ListAccessTokensResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.auth.RevokeAccessTokenResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.browse.BrowsePathsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.browse.BrowseResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.chart.ChartStatsSummaryResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.config.AppConfigResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.container.ContainerEntitiesResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.container.ParentContainersResolver;
|
||||
@ -1080,6 +1081,7 @@ public class GmsGraphQLEngine {
|
||||
})
|
||||
)
|
||||
.dataFetcher("parentContainers", new ParentContainersResolver(entityClient))
|
||||
.dataFetcher("statsSummary", new ChartStatsSummaryResolver(this.timeseriesAspectService))
|
||||
);
|
||||
builder.type("ChartInfo", typeWiring -> typeWiring
|
||||
.dataFetcher("inputs", new LoadableTypeBatchResolver<>(datasetType,
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.chart;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.datahub.graphql.generated.ChartStatsSummary;
|
||||
import com.linkedin.metadata.timeseries.TimeseriesAspectService;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
@Slf4j
|
||||
public class ChartStatsSummaryResolver implements DataFetcher<CompletableFuture<ChartStatsSummary>> {
|
||||
|
||||
private final TimeseriesAspectService timeseriesAspectService;
|
||||
private final Cache<Urn, ChartStatsSummary> summaryCache;
|
||||
|
||||
public ChartStatsSummaryResolver(final TimeseriesAspectService timeseriesAspectService) {
|
||||
this.timeseriesAspectService = timeseriesAspectService;
|
||||
this.summaryCache = CacheBuilder.newBuilder()
|
||||
.maximumSize(10000)
|
||||
.expireAfterWrite(6, TimeUnit.HOURS)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChartStatsSummary> get(DataFetchingEnvironment environment) throws Exception {
|
||||
// Not yet implemented
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
}
|
||||
@ -4139,7 +4139,7 @@ type Dashboard implements EntityWithRelationships & Entity {
|
||||
usageStats(startTimeMillis: Long, endTimeMillis: Long, limit: Int): DashboardUsageQueryResult
|
||||
|
||||
"""
|
||||
Experimental - Summary operational & usage statistics about a Dataset
|
||||
Experimental - Summary operational & usage statistics about a Dashboard
|
||||
"""
|
||||
statsSummary: DashboardStatsSummary
|
||||
|
||||
@ -4396,6 +4396,13 @@ type Chart implements EntityWithRelationships & Entity {
|
||||
"""
|
||||
dataPlatformInstance: DataPlatformInstance
|
||||
|
||||
"""
|
||||
Not yet implemented.
|
||||
|
||||
Experimental - Summary operational & usage statistics about a Chart
|
||||
"""
|
||||
statsSummary: ChartStatsSummary
|
||||
|
||||
"""
|
||||
Granular API for querying edges extending from this entity
|
||||
"""
|
||||
@ -5467,6 +5474,32 @@ type DashboardStatsSummary {
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
Experimental - subject to change. A summary of usage metrics about a Chart.
|
||||
"""
|
||||
type ChartStatsSummary {
|
||||
"""
|
||||
The total view count for the chart
|
||||
"""
|
||||
viewCount: Int
|
||||
|
||||
"""
|
||||
The view count in the last 30 days
|
||||
"""
|
||||
viewCountLast30Days: Int
|
||||
|
||||
"""
|
||||
The unique user count in the past 30 days
|
||||
"""
|
||||
uniqueUserCountLast30Days: Int
|
||||
|
||||
"""
|
||||
The top users in the past 30 days
|
||||
"""
|
||||
topUsersLast30Days: [CorpUser!]
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
The duration of a fixed window of time
|
||||
"""
|
||||
|
||||
@ -17,6 +17,7 @@ import { getDataForEntityType } from '../shared/containers/profile/utils';
|
||||
import { SidebarDomainSection } from '../shared/containers/profile/sidebar/Domain/SidebarDomainSection';
|
||||
import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown';
|
||||
import { LineageTab } from '../shared/tabs/Lineage/LineageTab';
|
||||
import { ChartStatsSummarySubHeader } from './profile/stats/ChartStatsSummarySubHeader';
|
||||
|
||||
/**
|
||||
* Definition of the DataHub Chart entity.
|
||||
@ -71,6 +72,9 @@ export class ChartEntity implements Entity<Chart> {
|
||||
useUpdateQuery={useUpdateChartMutation}
|
||||
getOverrideProperties={this.getOverridePropertiesFromEntity}
|
||||
headerDropdownItems={new Set([EntityMenuItems.COPY_URL, EntityMenuItems.UPDATE_DEPRECATION])}
|
||||
subHeader={{
|
||||
component: ChartStatsSummarySubHeader,
|
||||
}}
|
||||
tabs={[
|
||||
{
|
||||
name: 'Documentation',
|
||||
@ -176,6 +180,10 @@ export class ChartEntity implements Entity<Chart> {
|
||||
logoUrl={data?.platform?.properties?.logoUrl || ''}
|
||||
domain={data.domain?.domain}
|
||||
deprecation={data.deprecation}
|
||||
statsSummary={data.statsSummary}
|
||||
lastUpdatedMs={data.properties?.lastModified?.time}
|
||||
createdMs={data.properties?.created?.time}
|
||||
externalUrl={data.properties?.externalUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -10,11 +10,13 @@ import {
|
||||
SearchInsight,
|
||||
ParentContainersResult,
|
||||
Deprecation,
|
||||
ChartStatsSummary,
|
||||
} from '../../../../types.generated';
|
||||
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import { capitalizeFirstLetter } from '../../../shared/textUtil';
|
||||
import { IconStyleType } from '../../Entity';
|
||||
import { ChartStatsSummary as ChartStatsSummaryView } from '../shared/ChartStatsSummary';
|
||||
|
||||
export const ChartPreview = ({
|
||||
urn,
|
||||
@ -31,6 +33,10 @@ export const ChartPreview = ({
|
||||
insights,
|
||||
logoUrl,
|
||||
deprecation,
|
||||
statsSummary,
|
||||
lastUpdatedMs,
|
||||
createdMs,
|
||||
externalUrl,
|
||||
parentContainers,
|
||||
}: {
|
||||
urn: string;
|
||||
@ -47,6 +53,10 @@ export const ChartPreview = ({
|
||||
insights?: Array<SearchInsight> | null;
|
||||
logoUrl?: string | null;
|
||||
deprecation?: Deprecation | null;
|
||||
statsSummary?: ChartStatsSummary | null;
|
||||
lastUpdatedMs?: number | null;
|
||||
createdMs?: number | null;
|
||||
externalUrl?: string | null;
|
||||
parentContainers?: ParentContainersResult | null;
|
||||
}): JSX.Element => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
@ -71,6 +81,15 @@ export const ChartPreview = ({
|
||||
insights={insights}
|
||||
parentContainers={parentContainers}
|
||||
deprecation={deprecation}
|
||||
externalUrl={externalUrl}
|
||||
subHeader={
|
||||
<ChartStatsSummaryView
|
||||
viewCount={statsSummary?.viewCount}
|
||||
uniqueUserCountLast30Days={statsSummary?.uniqueUserCountLast30Days}
|
||||
lastUpdatedMs={lastUpdatedMs}
|
||||
createdMs={createdMs}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { ChartStatsSummary as ChartStatsSummaryObj } from '../../../../../types.generated';
|
||||
import { useBaseEntity } from '../../../shared/EntityContext';
|
||||
import { GetChartQuery } from '../../../../../graphql/chart.generated';
|
||||
import { ChartStatsSummary } from '../../shared/ChartStatsSummary';
|
||||
|
||||
export const ChartStatsSummarySubHeader = () => {
|
||||
const result = useBaseEntity<GetChartQuery>();
|
||||
const chart = result?.chart;
|
||||
const maybeStatsSummary = chart?.statsSummary as ChartStatsSummaryObj;
|
||||
const viewCount = maybeStatsSummary?.viewCount;
|
||||
const uniqueUserCountLast30Days = maybeStatsSummary?.uniqueUserCountLast30Days;
|
||||
const lastUpdatedMs = chart?.properties?.lastModified?.time;
|
||||
const createdMs = chart?.properties?.created?.time;
|
||||
|
||||
return (
|
||||
<ChartStatsSummary
|
||||
viewCount={viewCount}
|
||||
uniqueUserCountLast30Days={uniqueUserCountLast30Days}
|
||||
lastUpdatedMs={lastUpdatedMs}
|
||||
createdMs={createdMs}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Popover, Tooltip } from 'antd';
|
||||
import { ClockCircleOutlined, EyeOutlined, TeamOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { formatNumberWithoutAbbreviation } from '../../../shared/formatNumber';
|
||||
import { ANTD_GRAY } from '../../shared/constants';
|
||||
import { toLocalDateTimeString, toRelativeTimeString } from '../../../shared/time/timeUtils';
|
||||
import { StatsSummary } from '../../shared/components/styled/StatsSummary';
|
||||
|
||||
const StatText = styled.span`
|
||||
color: ${ANTD_GRAY[8]};
|
||||
`;
|
||||
|
||||
const HelpIcon = styled(QuestionCircleOutlined)`
|
||||
color: ${ANTD_GRAY[7]};
|
||||
padding-left: 4px;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
chartCount?: number | null;
|
||||
viewCount?: number | null;
|
||||
uniqueUserCountLast30Days?: number | null;
|
||||
lastUpdatedMs?: number | null;
|
||||
createdMs?: number | null;
|
||||
};
|
||||
|
||||
export const ChartStatsSummary = ({
|
||||
chartCount,
|
||||
viewCount,
|
||||
uniqueUserCountLast30Days,
|
||||
lastUpdatedMs,
|
||||
createdMs,
|
||||
}: Props) => {
|
||||
const statsViews = [
|
||||
(!!chartCount && (
|
||||
<StatText>
|
||||
<b>{chartCount}</b> charts
|
||||
</StatText>
|
||||
)) ||
|
||||
undefined,
|
||||
(!!viewCount && (
|
||||
<StatText>
|
||||
<EyeOutlined style={{ marginRight: 8, color: ANTD_GRAY[7] }} />
|
||||
<b>{formatNumberWithoutAbbreviation(viewCount)}</b> views
|
||||
</StatText>
|
||||
)) ||
|
||||
undefined,
|
||||
(!!uniqueUserCountLast30Days && (
|
||||
<StatText>
|
||||
<TeamOutlined style={{ marginRight: 8, color: ANTD_GRAY[7] }} />
|
||||
<b>{formatNumberWithoutAbbreviation(uniqueUserCountLast30Days)}</b> unique users
|
||||
</StatText>
|
||||
)) ||
|
||||
undefined,
|
||||
(!!lastUpdatedMs && (
|
||||
<Popover
|
||||
content={
|
||||
<>
|
||||
{createdMs && <div>Created on {toLocalDateTimeString(createdMs)}.</div>}
|
||||
<div>
|
||||
Changed on {toLocalDateTimeString(lastUpdatedMs)}.{' '}
|
||||
<Tooltip title="The time at which the chart was last changed in the source platform">
|
||||
<HelpIcon />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<StatText>
|
||||
<ClockCircleOutlined style={{ marginRight: 8, color: ANTD_GRAY[7] }} />
|
||||
Changed {toRelativeTimeString(lastUpdatedMs)}
|
||||
</StatText>
|
||||
</Popover>
|
||||
)) ||
|
||||
undefined,
|
||||
].filter((stat) => stat !== undefined);
|
||||
|
||||
return <>{statsViews.length > 0 && <StatsSummary stats={statsViews} />}</>;
|
||||
};
|
||||
@ -71,6 +71,25 @@ query getChart($urn: String!) {
|
||||
dataPlatformInstance {
|
||||
...dataPlatformInstanceFields
|
||||
}
|
||||
statsSummary {
|
||||
viewCount
|
||||
uniqueUserCountLast30Days
|
||||
topUsersLast30Days {
|
||||
urn
|
||||
type
|
||||
username
|
||||
properties {
|
||||
displayName
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
editableProperties {
|
||||
displayName
|
||||
pictureLink
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -385,6 +385,9 @@ fragment searchResultFields on Entity {
|
||||
lastModified {
|
||||
time
|
||||
}
|
||||
created {
|
||||
time
|
||||
}
|
||||
}
|
||||
ownership {
|
||||
...ownershipFields
|
||||
@ -413,6 +416,25 @@ fragment searchResultFields on Entity {
|
||||
parentContainers {
|
||||
...parentContainersFields
|
||||
}
|
||||
statsSummary {
|
||||
viewCount
|
||||
uniqueUserCountLast30Days
|
||||
topUsersLast30Days {
|
||||
urn
|
||||
type
|
||||
username
|
||||
properties {
|
||||
displayName
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
editableProperties {
|
||||
displayName
|
||||
pictureLink
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataFlow {
|
||||
flowId
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
namespace com.linkedin.chart
|
||||
|
||||
import com.linkedin.timeseries.TimeseriesAspectBase
|
||||
|
||||
/**
|
||||
* Experimental (Subject to breaking change) -- Stats corresponding to chart's usage.
|
||||
*
|
||||
* If this aspect represents the latest snapshot of the statistics about a Chart, the eventGranularity field should be null.
|
||||
* If this aspect represents a bucketed window of usage statistics (e.g. over a day), then the eventGranularity field should be set accordingly.
|
||||
*/
|
||||
@Aspect = {
|
||||
"name": "chartUsageStatistics",
|
||||
"type": "timeseries",
|
||||
}
|
||||
record ChartUsageStatistics includes TimeseriesAspectBase {
|
||||
/**
|
||||
* The total number of times chart has been viewed
|
||||
*/
|
||||
@TimeseriesField = {}
|
||||
viewsCount: optional int
|
||||
|
||||
/**
|
||||
* Unique user count
|
||||
*/
|
||||
@TimeseriesField = {}
|
||||
uniqueUserCount: optional int
|
||||
|
||||
/**
|
||||
* Users within this bucket, with frequency counts
|
||||
*/
|
||||
@TimeseriesFieldCollection = {"key":"user"}
|
||||
userCounts: optional array[ChartUserUsageCounts]
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
namespace com.linkedin.chart
|
||||
|
||||
import com.linkedin.common.Urn
|
||||
|
||||
/**
|
||||
* Records a single user's usage counts for a given resource
|
||||
*/
|
||||
record ChartUserUsageCounts {
|
||||
/**
|
||||
* The unique id of the user.
|
||||
*/
|
||||
user: Urn
|
||||
|
||||
/**
|
||||
* The number of times the user has viewed the chart
|
||||
*/
|
||||
@TimeseriesField = {}
|
||||
viewsCount: optional int
|
||||
}
|
||||
@ -99,6 +99,7 @@ public class Constants {
|
||||
public static final String CHART_INFO_ASPECT_NAME = "chartInfo";
|
||||
public static final String EDITABLE_CHART_PROPERTIES_ASPECT_NAME = "editableChartProperties";
|
||||
public static final String CHART_QUERY_ASPECT_NAME = "chartQuery";
|
||||
public static final String CHART_USAGE_STATISTICS_ASPECT_NAME = "chartUsageStatistics";
|
||||
|
||||
// Dashboard
|
||||
public static final String DASHBOARD_KEY_ASPECT_NAME = "dashboardKey";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user