Skip to content

Commit

Permalink
feat(ui): Partial support for Chart usage (datahub-project#5473)
Browse files Browse the repository at this point in the history
  • Loading branch information
jjoyce0510 authored and maggiehays committed Aug 1, 2022
1 parent 5bf88c2 commit 12c7253
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
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;
Expand Down Expand Up @@ -1080,6 +1081,7 @@ private void configureChartResolvers(final RuntimeWiring.Builder builder) {
})
)
.dataFetcher("parentContainers", new ParentContainersResolver(entityClient))
.dataFetcher("statsSummary", new ChartStatsSummaryResolver(this.timeseriesAspectService))
);
builder.type("ChartInfo", typeWiring -> typeWiring
.dataFetcher("inputs", new LoadableTypeBatchResolver<>(datasetType,
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
35 changes: 34 additions & 1 deletion datahub-graphql-core/src/main/resources/entity.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
"""
Expand Down Expand Up @@ -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
"""
Expand Down
8 changes: 8 additions & 0 deletions datahub-web-react/src/app/entity/chart/ChartEntity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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}
/>
);
};
Expand Down
19 changes: 19 additions & 0 deletions datahub-web-react/src/app/entity/chart/preview/ChartPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -31,6 +33,10 @@ export const ChartPreview = ({
insights,
logoUrl,
deprecation,
statsSummary,
lastUpdatedMs,
createdMs,
externalUrl,
parentContainers,
}: {
urn: string;
Expand All @@ -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();
Expand All @@ -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}
/>
}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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} />}</>;
};
19 changes: 19 additions & 0 deletions datahub-web-react/src/graphql/chart.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
}
}

Expand Down
22 changes: 22 additions & 0 deletions datahub-web-react/src/graphql/search.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,9 @@ fragment searchResultFields on Entity {
lastModified {
time
}
created {
time
}
}
ownership {
...ownershipFields
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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]
}
Loading

0 comments on commit 12c7253

Please sign in to comment.