Skip to content
Open
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 @@ -2,9 +2,12 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.linkedin.datahub.graphql.generated.ProductUpdate;
import com.linkedin.datahub.graphql.generated.ProductUpdateFeature;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -64,33 +67,138 @@ public static ProductUpdate parseProductUpdate(

String id = json.get("id").asText();
String title = json.get("title").asText();
String ctaText = json.has("ctaText") ? json.get("ctaText").asText() : "Learn more";
String ctaLink = json.has("ctaLink") ? json.get("ctaLink").asText() : "";

// Decorate ctaLink with clientId if provided
if (clientId != null && !clientId.trim().isEmpty() && !ctaLink.isEmpty()) {
ctaLink = decorateUrlWithClientId(ctaLink, clientId);
}

// Build the ProductUpdate response
ProductUpdate productUpdate = new ProductUpdate();
productUpdate.setEnabled(enabled);
productUpdate.setId(id);
productUpdate.setTitle(title);
productUpdate.setCtaText(ctaText);
productUpdate.setCtaLink(ctaLink);

// Optional fields
if (json.has("header")) {
productUpdate.setHeader(json.get("header").asText());
}
if (json.has("requiredVersion")) {
productUpdate.setRequiredVersion(json.get("requiredVersion").asText());
}
if (json.has("description")) {
productUpdate.setDescription(json.get("description").asText());
}
if (json.has("image")) {
productUpdate.setImage(json.get("image").asText());
}

// Parse primary CTA (new format) - preferred over legacy ctaText/ctaLink
if (json.has("primaryCtaText") && json.has("primaryCtaLink")) {
String primaryCtaText = json.get("primaryCtaText").asText();
String primaryCtaLink = maybeDecorateUrl(json.get("primaryCtaLink").asText(), clientId);

productUpdate.setPrimaryCtaText(primaryCtaText);
productUpdate.setPrimaryCtaLink(primaryCtaLink);
}

// Parse secondary CTA (optional)
if (json.has("secondaryCtaText") && json.has("secondaryCtaLink")) {
String secondaryCtaText = json.get("secondaryCtaText").asText();
String secondaryCtaLink = maybeDecorateUrl(json.get("secondaryCtaLink").asText(), clientId);

productUpdate.setSecondaryCtaText(secondaryCtaText);
productUpdate.setSecondaryCtaLink(secondaryCtaLink);
}

// Parse legacy CTA fields (backward compatibility)
// Only use if primary CTA is not provided
if (!json.has("primaryCtaText") || !json.has("primaryCtaLink")) {
String ctaText = json.has("ctaText") ? json.get("ctaText").asText() : "Learn more";
String ctaLink =
maybeDecorateUrl(json.has("ctaLink") ? json.get("ctaLink").asText() : "", clientId);

productUpdate.setCtaText(ctaText);
productUpdate.setCtaLink(ctaLink);
}

// Parse features array if present
if (json.has("features") && json.get("features").isArray()) {
List<ProductUpdateFeature> features = parseFeatures(json.get("features"));
if (!features.isEmpty()) {
productUpdate.setFeatures(features);
}
}

return productUpdate;
}

/**
* Parse features array from JSON.
*
* @param featuresArray JSON array node containing feature objects
* @return List of parsed ProductUpdateFeature objects (may be empty)
*/
@Nonnull
private static List<ProductUpdateFeature> parseFeatures(@Nonnull JsonNode featuresArray) {
List<ProductUpdateFeature> features = new ArrayList<>();

for (JsonNode featureNode : featuresArray) {
ProductUpdateFeature feature = parseFeature(featureNode);
if (feature != null) {
features.add(feature);
}
}

return features;
}

/**
* Parse a single feature from JSON.
*
* @param featureNode JSON node containing a feature object
* @return Parsed ProductUpdateFeature, or null if parsing fails or required fields are missing
*/
@Nullable
private static ProductUpdateFeature parseFeature(@Nonnull JsonNode featureNode) {
// Validate required fields
if (!featureNode.has("title") || !featureNode.has("description")) {
log.warn("Skipping invalid feature entry: missing required fields (title or description)");
return null;
}

try {
ProductUpdateFeature feature = new ProductUpdateFeature();
feature.setTitle(featureNode.get("title").asText());
feature.setDescription(featureNode.get("description").asText());

// Icon is optional
if (featureNode.has("icon")) {
feature.setIcon(featureNode.get("icon").asText());
}

// Availability is optional
if (featureNode.has("availability")) {
feature.setAvailability(featureNode.get("availability").asText());
}

return feature;
} catch (Exception e) {
log.warn("Failed to parse feature entry, skipping: {}", e.getMessage());
return null;
}
}

/**
* Conditionally decorates a URL with clientId if the clientId is valid and URL is non-empty.
*
* @param url The URL to potentially decorate (may be empty)
* @param clientId The client ID to append (may be null or empty)
* @return The decorated URL if conditions are met, otherwise the original URL
*/
@Nonnull
private static String maybeDecorateUrl(@Nonnull String url, @Nullable String clientId) {
if (clientId != null && !clientId.trim().isEmpty() && !url.isEmpty()) {
return decorateUrlWithClientId(url, clientId);
}
return url;
}

/**
* Decorates a URL with a clientId query parameter.
*
Expand Down
72 changes: 68 additions & 4 deletions datahub-graphql-core/src/main/resources/app.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,32 @@ type GlobalHomePageSettings {
defaultTemplate: DataHubPageTemplate
}

"""
Feature information for a product update
"""
type ProductUpdateFeature {
"""
Title of the feature (subheader)
"""
title: String!

"""
Description text for the feature (bullet text)
"""
description: String!

"""
Phosphor icon name (PascalCase, e.g., "Lightning", "Sparkle", "Settings", "Domain")
Optional - if not provided, a default bullet will be shown
"""
icon: String

"""
Optional availability text (e.g., "Available in DataHub Cloud")
"""
availability: String
}

"""
Product update information fetched from remote JSON source
"""
Expand All @@ -941,6 +967,16 @@ type ProductUpdate {
"""
title: String!

"""
Optional header text (displayed instead of title if provided)
"""
header: String

"""
Optional minimum required version for this update
"""
requiredVersion: String

"""
Optional URL to an image to display with the update
"""
Expand All @@ -952,12 +988,40 @@ type ProductUpdate {
description: String

"""
Call-to-action button text (e.g., "Read updates")
Primary call-to-action button text (required if primaryCtaLink is provided)
"""
primaryCtaText: String

"""
Primary call-to-action link URL, with telemetry client ID appended (required if primaryCtaText is provided)
Relative URLs will be prefixed with baseUrl
"""
primaryCtaLink: String

"""
Secondary call-to-action button text (optional)
"""
secondaryCtaText: String

"""
Secondary call-to-action link URL, with telemetry client ID appended (optional)
"""
secondaryCtaLink: String

"""
Call-to-action button text (deprecated, use primaryCtaText instead)
Kept for backward compatibility
"""
ctaText: String

"""
Call-to-action link URL, with telemetry client ID appended (deprecated, use primaryCtaLink instead)
Kept for backward compatibility
"""
ctaText: String!
ctaLink: String

"""
Call-to-action link URL, with telemetry client ID appended
Optional list of features (up to 3) to display with icons and descriptions
"""
ctaLink: String!
features: [ProductUpdateFeature!]
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import useSelectedKey from '@app/homeV2/layout/navBarRedesign/useSelectedKey';
import { useShowHomePageRedesign } from '@app/homeV3/context/hooks/useShowHomePageRedesign';
import OnboardingContext from '@app/onboarding/OnboardingContext';
import { useOnboardingTour } from '@app/onboarding/OnboardingTourContext.hooks';
import { NAV_SIDEBAR_ID, NAV_SIDEBAR_WIDTH_COLLAPSED, NAV_SIDEBAR_WIDTH_EXPANDED } from '@app/shared/constants';
import { useIsHomePage } from '@app/shared/useIsHomePage';
import { useAppConfig, useBusinessAttributesFlag } from '@app/useAppConfig';
import { colors } from '@src/alchemy-components';
Expand Down Expand Up @@ -60,7 +61,7 @@ const Content = styled.div<{ isCollapsed: boolean }>`
flex-direction: column;
padding: 17px 8px 17px 16px;
height: 100%;
width: ${(props) => (props.isCollapsed ? '60px' : '264px')};
width: ${(props) => (props.isCollapsed ? `${NAV_SIDEBAR_WIDTH_COLLAPSED}px` : `${NAV_SIDEBAR_WIDTH_EXPANDED}px`)};
transition: width 250ms ease-in-out;
overflow-x: hidden;
`;
Expand Down Expand Up @@ -347,7 +348,7 @@ export const NavSidebar = () => {
return (
<Container>
{renderSvgSelectedGradientForReusingInIcons()}
<Content isCollapsed={isCollapsed}>
<Content id={NAV_SIDEBAR_ID} data-collapsed={isCollapsed} isCollapsed={isCollapsed}>
{showSkeleton ? (
<NavSkeleton isCollapsed={isCollapsed} />
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { useEffect, useRef, useState } from 'react';
import analytics, { EventType } from '@app/analytics';
import { useOnboardingTour } from '@app/onboarding/OnboardingTourContext.hooks';
import { ANT_NOTIFICATION_Z_INDEX } from '@app/shared/constants';
import { checkShouldSkipWelcomeModal, setSkipWelcomeModal } from '@app/shared/localStorageUtils';
import {
LoadingContainer,
SlideContainer,
Expand All @@ -17,7 +18,6 @@ import welcomeModalHomeScreenshot from '@images/welcome-modal-home-screenshot.pn
const SLIDE_DURATION_MS = 10000;
const DATAHUB_DOCS_URL = 'https://docs.datahub.com/docs/category/features';
const WELCOME_TO_DATAHUB_MODAL_TITLE = 'Welcome to DataHub';
const SKIP_WELCOME_MODAL_KEY = 'skipWelcomeModal';

interface VideoSources {
search: string;
Expand All @@ -26,10 +26,6 @@ interface VideoSources {
aiDocs?: string;
}

function checkShouldSkipWelcomeModal() {
return localStorage.getItem(SKIP_WELCOME_MODAL_KEY) === 'true';
}

export const WelcomeToDataHubModal = () => {
const [shouldShow, setShouldShow] = useState(false);
const [currentSlide, setCurrentSlide] = useState(0);
Expand Down Expand Up @@ -147,7 +143,7 @@ export const WelcomeToDataHubModal = () => {
closeModalTour();
} else {
// Only set localStorage for automatic first-time tours, not manual triggers
localStorage.setItem(SKIP_WELCOME_MODAL_KEY, 'true');
setSkipWelcomeModal(true);
}
}

Expand Down
9 changes: 9 additions & 0 deletions datahub-web-react/src/app/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@ export const ANT_NOTIFICATION_Z_INDEX = 1010;

// S3 folder to store product assets
export const PRODUCT_ASSETS_FOLDER = 'product_assets';

// LocalStorage keys for dismissal/skip states
export const SKIP_WELCOME_MODAL_KEY = 'skipWelcomeModal';
export const DISMISSED_PRODUCT_UPDATES_KEY = 'dismissedProductUpdates';

// Navigation sidebar
export const NAV_SIDEBAR_ID = 'nav-sidebar';
export const NAV_SIDEBAR_WIDTH_EXPANDED = 264;
export const NAV_SIDEBAR_WIDTH_COLLAPSED = 60;
Loading
Loading