Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): Partial support for Chart usage #5473

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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