mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-28 10:28:22 +00:00
feat(ui): Adding release pop up in disabled state for v0.1.2 (#14066)
Co-authored-by: John Joyce <john@Mac-900.lan> Co-authored-by: John Joyce <john@Mac-923.lan> Co-authored-by: John Joyce <john@Mac-1136.lan> Co-authored-by: John Joyce <john@Mac-2320.lan> Co-authored-by: John Joyce <john@Mac.lan> Co-authored-by: John Joyce <john@ip-192-168-1-63.us-west-2.compute.internal> Co-authored-by: John Joyce <john@Mac-4711.lan>
This commit is contained in:
parent
4b764ed55f
commit
ff40a94908
@ -281,6 +281,7 @@ public class AppConfigResolver implements DataFetcher<CompletableFuture<AppConfi
|
||||
.setShowIngestionPageRedesign(_featureFlags.isShowIngestionPageRedesign())
|
||||
.setShowLineageExpandMore(_featureFlags.isShowLineageExpandMore())
|
||||
.setShowHomePageRedesign(_featureFlags.isShowHomePageRedesign())
|
||||
.setShowProductUpdates(_featureFlags.isShowProductUpdates())
|
||||
.setLineageGraphV3(_featureFlags.isLineageGraphV3())
|
||||
.build();
|
||||
|
||||
|
||||
@ -748,6 +748,11 @@ type FeatureFlagsConfig {
|
||||
"""
|
||||
showHomePageRedesign: Boolean!
|
||||
|
||||
"""
|
||||
Whether product updates on the sidebar is enabled. Will go to oss.
|
||||
"""
|
||||
showProductUpdates: Boolean!
|
||||
|
||||
"""
|
||||
Enables the redesign of the lineage v2 graph
|
||||
"""
|
||||
|
||||
@ -119,6 +119,7 @@ export enum EventType {
|
||||
ShowAllVersionsEvent,
|
||||
HomePageClick,
|
||||
SearchBarFilter,
|
||||
ClickProductUpdate,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -882,6 +883,12 @@ export interface SearchBarFilterEvent extends BaseEvent {
|
||||
values: string[]; // the values being filtered for
|
||||
}
|
||||
|
||||
export interface ClickProductUpdateEvent extends BaseEvent {
|
||||
type: EventType.ClickProductUpdate;
|
||||
id: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event consisting of a union of specific event types.
|
||||
*/
|
||||
@ -985,4 +992,5 @@ export type Event =
|
||||
| UnlinkAssetVersionEvent
|
||||
| ShowAllVersionsEvent
|
||||
| HomePageClickEvent
|
||||
| SearchBarFilterEvent;
|
||||
| SearchBarFilterEvent
|
||||
| ClickProductUpdateEvent;
|
||||
|
||||
@ -10,6 +10,7 @@ import { SearchHeader } from '@app/searchV2/SearchHeader';
|
||||
import useGoToSearchPage from '@app/searchV2/useGoToSearchPage';
|
||||
import useQueryAndFiltersFromLocation from '@app/searchV2/useQueryAndFiltersFromLocation';
|
||||
import { getAutoCompleteInputFromQuickFilter } from '@app/searchV2/utils/filterUtils';
|
||||
import ProductUpdates from '@app/shared/product/update/ProductUpdates';
|
||||
import { useAppConfig } from '@app/useAppConfig';
|
||||
import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||
import { useShowNavBarRedesign } from '@app/useShowNavBarRedesign';
|
||||
@ -141,6 +142,7 @@ export const SearchablePage = ({ children, hideSearchBar }: Props) => {
|
||||
</Navigation>
|
||||
<Content $isShowNavBarRedesign={isShowNavBarRedesign}>{children}</Content>
|
||||
</Body>
|
||||
<ProductUpdates />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,139 @@
|
||||
import { Tooltip, colors } from '@components';
|
||||
import { X } from '@phosphor-icons/react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import analytics, { EventType } from '@app/analytics';
|
||||
import {
|
||||
useDismissProductAnnouncement,
|
||||
useGetLatestProductAnnouncementData,
|
||||
useIsProductAnnouncementEnabled,
|
||||
useIsProductAnnouncementVisible,
|
||||
} from '@app/shared/product/update/hooks';
|
||||
|
||||
const CardWrapper = styled.div`
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 16px;
|
||||
width: 240px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0px 0px 6px 0px #5d668b33;
|
||||
background: white;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px 0px 12px;
|
||||
`;
|
||||
|
||||
const Title = styled.h3`
|
||||
font-size: 14px;
|
||||
font-family: Mulish;
|
||||
font-weight: 700;
|
||||
color: ${colors.gray[600]};
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #4b5563;
|
||||
}
|
||||
`;
|
||||
|
||||
const ImageSection = styled.div`
|
||||
margin: 0px 12px 12px 12px;
|
||||
`;
|
||||
|
||||
const Image = styled.img`
|
||||
width: 100%;
|
||||
height: auto;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
padding: 0px 12px 12px 12px;
|
||||
`;
|
||||
|
||||
const Description = styled.p`
|
||||
font-size: 14px;
|
||||
color: ${colors.gray[1700]};
|
||||
`;
|
||||
|
||||
const CTA = styled.a`
|
||||
font-size: 14px;
|
||||
color: ${colors.primary[500]};
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledCloseRounded = styled(X)`
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
export default function ProductUpdates() {
|
||||
const isFeatureEnabled = useIsProductAnnouncementEnabled();
|
||||
const latestUpdate = useGetLatestProductAnnouncementData();
|
||||
const { title, image, description, ctaText, ctaLink } = latestUpdate;
|
||||
|
||||
const { visible, refetch } = useIsProductAnnouncementVisible(latestUpdate);
|
||||
const dismiss = useDismissProductAnnouncement(latestUpdate, refetch);
|
||||
|
||||
// Local state to hide immediately on dismiss
|
||||
const [isLocallyVisible, setIsLocallyVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLocallyVisible(visible);
|
||||
}, [visible]);
|
||||
|
||||
const handleDismiss = () => {
|
||||
setIsLocallyVisible(false);
|
||||
dismiss();
|
||||
};
|
||||
|
||||
const trackClick = () => {
|
||||
analytics.event({
|
||||
type: EventType.ClickProductUpdate,
|
||||
id: latestUpdate.id,
|
||||
url: latestUpdate.ctaLink,
|
||||
});
|
||||
};
|
||||
|
||||
if (!isFeatureEnabled || !isLocallyVisible || !latestUpdate.enabled) return null;
|
||||
|
||||
return (
|
||||
<CardWrapper>
|
||||
<Header>
|
||||
<Title>{title}</Title>
|
||||
<Tooltip title="Dismiss" placement="right">
|
||||
<CloseButton onClick={handleDismiss}>
|
||||
<StyledCloseRounded />
|
||||
</CloseButton>
|
||||
</Tooltip>
|
||||
</Header>
|
||||
{image && (
|
||||
<ImageSection>
|
||||
<Image src={image} alt="" />
|
||||
</ImageSection>
|
||||
)}
|
||||
<Content>
|
||||
{description && <Description>{description}</Description>}
|
||||
{ctaText && ctaLink && (
|
||||
<CTA href={ctaLink} onClick={trackClick} target="_blank" rel="noreferrer noopener">
|
||||
{ctaText} →
|
||||
</CTA>
|
||||
)}
|
||||
</Content>
|
||||
</CardWrapper>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,166 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
|
||||
import * as useUserContextModule from '@app/context/useUserContext';
|
||||
import {
|
||||
useGetLatestProductAnnouncementData,
|
||||
useIsProductAnnouncementEnabled,
|
||||
useIsProductAnnouncementVisible,
|
||||
} from '@app/shared/product/update/hooks';
|
||||
import { latestUpdate } from '@app/shared/product/update/latestUpdate';
|
||||
import * as useAppConfigModule from '@app/useAppConfig';
|
||||
|
||||
import { BatchGetStepStatesDocument } from '@graphql/step.generated';
|
||||
|
||||
const STEP_ID = 'urn:li:user:123-product_updates-test';
|
||||
const TEST_UPDATE = {
|
||||
enabled: true,
|
||||
id: 'test',
|
||||
title: "What's New In DataHub",
|
||||
description: 'Explore version v0.3.12',
|
||||
ctaText: 'Read updates',
|
||||
ctaLink: 'https://docs.yourapp.com/releases/v0.3.12',
|
||||
};
|
||||
|
||||
const BATCH_GET_STEP_STATES_MOCK_PRESENT = {
|
||||
request: {
|
||||
query: BatchGetStepStatesDocument,
|
||||
variables: {
|
||||
input: {
|
||||
ids: [STEP_ID],
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
batchGetStepStates: {
|
||||
results: [
|
||||
{
|
||||
id: STEP_ID,
|
||||
properties: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const BATCH_GET_STEP_STATES_MOCK_NOT_PRESENT = {
|
||||
request: {
|
||||
query: BatchGetStepStatesDocument,
|
||||
variables: {
|
||||
input: {
|
||||
ids: [STEP_ID],
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
batchGetStepStates: {
|
||||
results: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const BATCH_GET_STEP_STATES_MOCK_LOADING = {
|
||||
request: {
|
||||
query: BatchGetStepStatesDocument,
|
||||
variables: {
|
||||
input: {
|
||||
ids: [STEP_ID],
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: undefined,
|
||||
loading: true,
|
||||
},
|
||||
};
|
||||
|
||||
describe('product update hooks', () => {
|
||||
describe('useIsProductAnnouncementEnabled', () => {
|
||||
it('returns true when feature flag is enabled', () => {
|
||||
vi.spyOn(useAppConfigModule, 'useAppConfig').mockReturnValue({
|
||||
config: {
|
||||
featureFlags: {
|
||||
showProductUpdates: true,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useIsProductAnnouncementEnabled());
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when feature flag is disabled', () => {
|
||||
vi.spyOn(useAppConfigModule, 'useAppConfig').mockReturnValue({
|
||||
config: {
|
||||
featureFlags: {
|
||||
showProductUpdates: false,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useIsProductAnnouncementEnabled());
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useGetLatestProductAnnouncementData', () => {
|
||||
it('returns latest update object', () => {
|
||||
const { result } = renderHook(() => useGetLatestProductAnnouncementData());
|
||||
expect(result.current).toBe(latestUpdate);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useIsProductAnnouncementVisible', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(useUserContextModule, 'useUserContext').mockReturnValue({
|
||||
user: { urn: 'urn:li:user:123' },
|
||||
} as any);
|
||||
});
|
||||
|
||||
it('returns visible=false when step state exists', async () => {
|
||||
const { result } = renderHook(() => useIsProductAnnouncementVisible(TEST_UPDATE), {
|
||||
wrapper: ({ children }) => (
|
||||
<MockedProvider mocks={[BATCH_GET_STEP_STATES_MOCK_PRESENT]} addTypename={false}>
|
||||
{children}
|
||||
</MockedProvider>
|
||||
),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.visible).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns visible=true when step state does not exist', async () => {
|
||||
const { result } = renderHook(() => useIsProductAnnouncementVisible(TEST_UPDATE), {
|
||||
wrapper: ({ children }) => (
|
||||
<MockedProvider mocks={[BATCH_GET_STEP_STATES_MOCK_NOT_PRESENT]} addTypename={false}>
|
||||
{children}
|
||||
</MockedProvider>
|
||||
),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.visible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns visible=false when query is loading', () => {
|
||||
const { result } = renderHook(() => useIsProductAnnouncementVisible(TEST_UPDATE), {
|
||||
wrapper: ({ children }) => (
|
||||
<MockedProvider mocks={[BATCH_GET_STEP_STATES_MOCK_LOADING]} addTypename={false}>
|
||||
{children}
|
||||
</MockedProvider>
|
||||
),
|
||||
});
|
||||
|
||||
expect(result.current.visible).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
94
datahub-web-react/src/app/shared/product/update/hooks.ts
Normal file
94
datahub-web-react/src/app/shared/product/update/hooks.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useUserContext } from '@app/context/useUserContext';
|
||||
import { ProductUpdate, latestUpdate } from '@app/shared/product/update/latestUpdate';
|
||||
import { useAppConfig } from '@app/useAppConfig';
|
||||
|
||||
import { useBatchGetStepStatesQuery, useBatchUpdateStepStatesMutation } from '@graphql/step.generated';
|
||||
|
||||
const PRODUCT_UPDATE_STEP_PREFIX = 'product_updates';
|
||||
|
||||
function buildProductUpdateStepId(userUrn: string, updateId: string): string {
|
||||
return `${userUrn}-${PRODUCT_UPDATE_STEP_PREFIX}-${updateId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether product announcements feature is enabled and viewabled.
|
||||
*/
|
||||
export function useIsProductAnnouncementEnabled() {
|
||||
const appConfig = useAppConfig();
|
||||
const { showProductUpdates } = appConfig.config.featureFlags;
|
||||
return showProductUpdates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch the announcement data (eventually can replace with fetch).
|
||||
*/
|
||||
export function useGetLatestProductAnnouncementData() {
|
||||
return latestUpdate;
|
||||
}
|
||||
|
||||
export type ProductAnnouncementResult = {
|
||||
visible: boolean;
|
||||
refetch: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to check if the announcement should be shown based on dismissal state
|
||||
*/
|
||||
export function useIsProductAnnouncementVisible(update: ProductUpdate): ProductAnnouncementResult {
|
||||
const userUrn = useUserContext()?.user?.urn;
|
||||
const productUpdateStepId = userUrn ? buildProductUpdateStepId(userUrn, update.id) : null;
|
||||
const productUpdateStepIds = productUpdateStepId ? [productUpdateStepId] : [];
|
||||
const { data, loading, error, refetch } = useBatchGetStepStatesQuery({
|
||||
skip: !userUrn,
|
||||
variables: { input: { ids: productUpdateStepIds } },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
if (loading || error) {
|
||||
return {
|
||||
visible: false,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
const visible =
|
||||
(data?.batchGetStepStates?.results &&
|
||||
!data?.batchGetStepStates?.results?.some((result) => result?.id === productUpdateStepId)) ||
|
||||
false;
|
||||
|
||||
return {
|
||||
visible,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional helper to dismiss the announcement (can also inline in `onClose`)
|
||||
*/
|
||||
export function useDismissProductAnnouncement(update: ProductUpdate, refetch: () => void): () => void {
|
||||
const userUrn = useUserContext()?.user?.urn;
|
||||
const productUpdateStepId = userUrn ? buildProductUpdateStepId(userUrn, update.id) : null;
|
||||
|
||||
const [batchUpdateStepStates] = useBatchUpdateStepStatesMutation();
|
||||
|
||||
return useCallback(() => {
|
||||
if (!productUpdateStepId) return;
|
||||
|
||||
const stepStates = [
|
||||
{
|
||||
id: productUpdateStepId,
|
||||
properties: [],
|
||||
},
|
||||
];
|
||||
|
||||
batchUpdateStepStates({
|
||||
variables: { input: { states: stepStates } },
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to dismiss product announcement:', error);
|
||||
})
|
||||
.finally(() => refetch());
|
||||
}, [productUpdateStepId, batchUpdateStepStates, refetch]);
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import SampleImage from '@images/sample-product-update-image.png';
|
||||
|
||||
export type ProductUpdate = {
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
title: string;
|
||||
image?: string;
|
||||
description?: string;
|
||||
ctaText: string;
|
||||
ctaLink: string;
|
||||
};
|
||||
|
||||
// NOTE: This is a place that OSS and Cloud diverge.
|
||||
/* Important: Change this section to adjust the system announcement shown in the bottom left corner of the product! */
|
||||
// TODO: Migrate this to be served via an aspect!
|
||||
export const latestUpdate: ProductUpdate = {
|
||||
enabled: true,
|
||||
id: 'v1.2.0', // Very important, when changed it will trigger the announcement to be re-displayed for a user.
|
||||
title: "What's New In DataHub",
|
||||
description: 'Explore version v1.2.0',
|
||||
image: SampleImage, // Import and use image.,
|
||||
ctaText: 'Read updates',
|
||||
ctaLink: 'https://docs.datahub.com/docs/releases#v1-2-0',
|
||||
};
|
||||
@ -81,6 +81,7 @@ export const DEFAULT_APP_CONFIG = {
|
||||
showIngestionPageRedesign: false,
|
||||
showLineageExpandMore: false,
|
||||
showHomePageRedesign: false,
|
||||
showProductUpdates: false,
|
||||
lineageGraphV3: false,
|
||||
},
|
||||
chromeExtensionConfig: {
|
||||
|
||||
@ -103,6 +103,7 @@ query appConfig {
|
||||
showIngestionPageRedesign
|
||||
showLineageExpandMore
|
||||
showHomePageRedesign
|
||||
showProductUpdates
|
||||
lineageGraphV3
|
||||
}
|
||||
chromeExtensionConfig {
|
||||
|
||||
BIN
datahub-web-react/src/images/sample-product-update-image.png
Normal file
BIN
datahub-web-react/src/images/sample-product-update-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@ -44,4 +44,5 @@ public class FeatureFlags {
|
||||
private boolean showLineageExpandMore = true;
|
||||
private boolean showHomePageRedesign = false;
|
||||
private boolean lineageGraphV3 = true;
|
||||
private boolean showProductUpdates = false;
|
||||
}
|
||||
|
||||
@ -629,6 +629,7 @@ featureFlags:
|
||||
showLineageExpandMore: ${SHOW_LINEAGE_EXPAND_MORE:true} # If turned on, show the expand more button (>>) in the lineage graph
|
||||
showHomePageRedesign: ${SHOW_HOME_PAGE_REDESIGN:false} # If turned on, show the re-designed home page
|
||||
lineageGraphV3: ${LINEAGE_GRAPH_V3:false} # Enables the redesign of the lineage v2 graph
|
||||
showProductUpdates: ${SHOW_PRODUCT_UPDATES:true} # Whether to show in-product update popover on new updates.
|
||||
|
||||
entityChangeEvents:
|
||||
enabled: ${ENABLE_ENTITY_CHANGE_EVENTS_HOOK:true}
|
||||
|
||||
@ -111,7 +111,8 @@ public enum DataHubUsageEventType {
|
||||
UPDATE_ASPECT_EVENT("UpdateAspectEvent"),
|
||||
ENTITY_EVENT("EntityEvent"),
|
||||
FAILED_LOGIN_EVENT("FailedLogInEvent"),
|
||||
DELETE_POLICY_EVENT("DeletePolicyEvent");
|
||||
DELETE_POLICY_EVENT("DeletePolicyEvent"),
|
||||
CLICK_PRODUCT_UPDATE_EVENT("ClickProductUpdateEvent");
|
||||
|
||||
private final String type;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user