Skip to content

Commit 6701b07

Browse files
feat: announcements widget (#926)
1 parent b427dc7 commit 6701b07

40 files changed

+994
-171
lines changed

.env.example

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
VITE_API_BASE_URL=
22
VITE_FRONTEND_VERSION=
3-
VITE_FEATURE_OS_KEY=
43
VITE_REO_KEY=

.github/workflows/release.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ jobs:
3434
env:
3535
VITE_API_BASE_URL: "/api/v1"
3636
VITE_FRONTEND_VERSION: ${{ steps.get_version.outputs.VERSION }}
37-
VITE_FEATURE_OS_KEY: ${{ secrets.FEATURE_OS_KEY }}
3837
VITE_REO_KEY: ${{ secrets.REO_KEY }}
3938

4039
- name: Generate Changelog

CLAUDE.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ Required environment variables (see `.env.example`):
5858
```bash
5959
VITE_API_BASE_URL=http://localhost:8080/api/v1 # ZenML Server API endpoint
6060
VITE_FRONTEND_VERSION=v0.17.0 # Optional: UI version number
61-
VITE_FEATURE_OS_KEY=<key> # Optional: Feature flag service key
6261
VITE_REO_KEY=<key> # Optional: Reo analytics key
6362
```
6463

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ To get started with the ZenML Dashboard, follow these steps:
7676
> The frontend and the server-url needs to be on the same domain, e.g. `localhost` for local development
7777

7878
> [!NOTE]
79-
> For local development you **don't** need to set `VITE_FEATURE_OS_KEY`
8079
> You can set the `VITE_FRONTEND_VERSION` to a version number, e.g `v0.17.0`. This value is used to read the UI Version from the environment
8180

8281
5. **Run Development Server:**

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
"class-variance-authority": "^0.7.1",
3838
"clsx": "^2.1.1",
3939
"elkjs": "^0.10.0",
40-
"featureos-widget": "^0.0.32",
4140
"immer": "^10.1.1",
4241
"json-schema-faker": "^0.5.9",
4342
"jwt-decode": "^4.0.0",

pnpm-lock.yaml

Lines changed: 0 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/projects/project-item.tsx

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1-
import { Box, Skeleton } from "@zenml-io/react-component-library/components/server";
1+
import Copy from "@/assets/icons/copy.svg?react";
22
import Hash from "@/assets/icons/hash.svg?react";
3-
import { Link } from "react-router-dom";
4-
import { routes } from "@/router/routes";
53
import Pipeline from "@/assets/icons/pipeline.svg?react";
64
import Terminal from "@/assets/icons/terminal.svg?react";
7-
import { useQuery } from "@tanstack/react-query";
5+
import Tick from "@/assets/icons/tick-circle.svg?react";
86
import { projectQueries } from "@/data/projects";
97
import { useCopy } from "@/lib/copy";
10-
import Tick from "@/assets/icons/tick-circle.svg?react";
11-
import Copy from "@/assets/icons/copy.svg?react";
8+
import { generateProjectImageUrl } from "@/lib/images";
9+
import { routes } from "@/router/routes";
10+
import { Project } from "@/types/projects";
11+
import { useQuery } from "@tanstack/react-query";
1212
import {
1313
Badge,
1414
Tooltip,
1515
TooltipContent,
1616
TooltipProvider,
1717
TooltipTrigger
1818
} from "@zenml-io/react-component-library";
19-
import { generateNumberFromSalt } from "@/lib/images";
20-
import { Project } from "@/types/projects";
19+
import { Box, Skeleton } from "@zenml-io/react-component-library/components/server";
20+
import { Link } from "react-router-dom";
2121
import { ProjectMenu } from "./project-menu";
2222

2323
type Props = {
@@ -27,9 +27,7 @@ type Props = {
2727

2828
export function ProjectItem({ project, isDefault = false }: Props) {
2929
const displayName = project.body?.display_name;
30-
const imageUrl = `https://public-flavor-logos.s3.eu-central-1.amazonaws.com/projects/${generateNumberFromSalt(
31-
project.name
32-
)}.jpg`;
30+
const imageUrl = generateProjectImageUrl(project.name);
3331
return (
3432
<Box className="group relative h-full overflow-hidden transition-all duration-300 hover:shadow-md">
3533
<div className="relative h-[180px] w-full">
Lines changed: 28 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { fetcher } from "@/data/fetch";
2-
import { analyticsServerUrl } from "@/lib/analytics";
1+
import { useAnalyticsEvent } from "@/data/analytics/event";
32
import { TrackEvent } from "@/types/analytics";
43
import { zodResolver } from "@hookform/resolvers/zod";
5-
import { useMutation } from "@tanstack/react-query";
64
import { useToast } from "@zenml-io/react-component-library";
75
import { useForm } from "react-hook-form";
86
import { useUpgradeContext } from "../Context";
@@ -20,53 +18,38 @@ export function useUpgradeForm() {
2018
}
2119
});
2220

23-
const submitFormMutation = useMutation({
24-
mutationFn: submitUpgradeForm,
21+
const submitFormMutation = useAnalyticsEvent({
2522
onSuccess: () => setSubmitSuccess(true),
2623
onError: (e) => {
27-
toast({
28-
emphasis: "subtle",
29-
status: "error",
30-
rounded: true,
31-
description: e.message
32-
});
24+
if (e instanceof Error) {
25+
toast({
26+
emphasis: "subtle",
27+
status: "error",
28+
rounded: true,
29+
description: e.message
30+
});
31+
}
3332
}
3433
});
3534

36-
async function handleSubmitForm(data: UpgradeForm, userId: string, isDebug: boolean) {
37-
submitFormMutation.mutate({
38-
...data,
39-
userId,
40-
isDebug
41-
});
35+
async function handleSubmitForm(
36+
{ company, email, name }: UpgradeForm,
37+
userId: string,
38+
isDebug: boolean
39+
) {
40+
const trackEvent: TrackEvent = {
41+
debug: isDebug,
42+
event: "Upgrade initiated",
43+
type: "track",
44+
user_id: userId,
45+
properties: {
46+
company,
47+
email,
48+
name
49+
}
50+
};
51+
52+
submitFormMutation.mutate(trackEvent);
4253
}
4354
return { form, submitFormMutation, handleSubmitForm };
4455
}
45-
46-
async function submitUpgradeForm({
47-
company,
48-
email,
49-
isDebug,
50-
name,
51-
userId
52-
}: UpgradeForm & { isDebug: boolean; userId: string }) {
53-
const trackEvent: TrackEvent = {
54-
debug: isDebug,
55-
event: "Upgrade initiated",
56-
type: "track",
57-
user_id: userId,
58-
properties: {
59-
company,
60-
email,
61-
name
62-
}
63-
};
64-
return fetcher(analyticsServerUrl, {
65-
method: "POST",
66-
credentials: "omit",
67-
headers: {
68-
"Content-Type": "application/json"
69-
},
70-
body: JSON.stringify([trackEvent])
71-
});
72-
}

src/components/DisplayDate.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,36 @@ import { prepareBackendTimestamp } from "@/lib/dates";
22

33
export function DisplayDate({
44
dateString,
5-
short = false
5+
short = false,
6+
justDate = false
67
}: {
78
dateString: string;
89
short?: boolean;
10+
justDate?: boolean;
911
}) {
1012
const date = prepareBackendTimestamp(dateString);
1113

12-
return <>{short ? formatShortDate(date) : date.toLocaleString()}</>;
14+
return <>{short ? formatShortDate(date, justDate) : date.toLocaleString()}</>;
1315
}
1416

15-
function formatShortDate(date: Date) {
17+
function formatShortDate(date: Date, justDate: boolean) {
1618
const dateOptions: Intl.DateTimeFormatOptions = {
1719
month: "short",
1820
day: "numeric",
1921
year: "numeric"
2022
};
23+
24+
const formattedDate = date.toLocaleDateString("en-US", dateOptions);
25+
if (justDate) {
26+
return formattedDate;
27+
}
28+
2129
const timeOptions: Intl.DateTimeFormatOptions = {
2230
hour: "numeric",
2331
minute: "numeric",
2432
hour12: false
2533
};
2634

27-
const formattedDate = date.toLocaleDateString("en-US", dateOptions);
2835
const formattedTime = date.toLocaleTimeString("en-US", timeOptions);
2936
return `${formattedDate} ${formattedTime}`;
3037
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Markdown } from "@/components/Markdown";
2+
import { useAnnouncements } from "@/data/announcements/announcements";
3+
import {
4+
Button,
5+
Dialog,
6+
DialogContent,
7+
DialogDescription,
8+
DialogTitle
9+
} from "@zenml-io/react-component-library";
10+
import { useEffect, useState } from "react";
11+
import { AnnouncementImage } from "../announcement-image";
12+
import { AnnouncementLabel } from "../announcement-label";
13+
import { AnnouncementLinks } from "../announcement-links";
14+
import { AnnouncementVideo } from "../announcement-video";
15+
import { announcementStore } from "../persist-announcement";
16+
import { AnnouncementHighlightPageIndicator } from "./page-indicator";
17+
import { useNewAnnouncementHighlights } from "./use-new-highlights";
18+
19+
export function AnnouncementHighlight() {
20+
const announcementsQuery = useAnnouncements();
21+
const newFeatureHighlights = [...useNewAnnouncementHighlights(announcementsQuery.data)].reverse();
22+
const [open, setOpen] = useState(false);
23+
const [currentPage, setCurrentPage] = useState(0);
24+
25+
useEffect(() => {
26+
if (newFeatureHighlights.length >= 1) {
27+
setOpen(true);
28+
}
29+
}, [newFeatureHighlights]);
30+
31+
function handleChange(open: boolean) {
32+
setOpen(open);
33+
setCurrentPage(0);
34+
if (!open) {
35+
announcementStore.setAnnouncementLastSeen("lastSeenHighlights");
36+
}
37+
}
38+
39+
function handleNext() {
40+
const isLast = currentPage === newFeatureHighlights.length - 1;
41+
if (isLast) {
42+
handleChange(false);
43+
} else {
44+
setCurrentPage((prev) => prev + 1);
45+
}
46+
}
47+
48+
const highlightedFeatureCount = newFeatureHighlights.length;
49+
if (highlightedFeatureCount === 0) return null;
50+
51+
const isLastPage = currentPage === highlightedFeatureCount - 1;
52+
const currentItem = newFeatureHighlights[currentPage];
53+
54+
return (
55+
<Dialog open={open} onOpenChange={handleChange}>
56+
<DialogContent className="flex max-w-[600px] flex-col overflow-hidden">
57+
{currentItem.video_url ? (
58+
<AnnouncementVideo videoUrl={currentItem.video_url} />
59+
) : (
60+
<AnnouncementImage title={currentItem.title} imageUrl={currentItem.feature_image_url} />
61+
)}
62+
<div className="space-y-5 p-5">
63+
<AnnouncementHighlightPageIndicator
64+
currentPage={currentPage}
65+
totalPages={highlightedFeatureCount}
66+
/>
67+
<div className="space-y-3">
68+
<div className="space-y-1">
69+
<DialogTitle className="text-display-xs font-semibold">
70+
{currentItem.title}
71+
</DialogTitle>
72+
<DialogDescription className="sr-only">
73+
Announcement Highlight: {currentItem.title}
74+
</DialogDescription>
75+
<ul className="flex flex-wrap items-center gap-0.5">
76+
{currentItem.labels.map((label, idx) => (
77+
<li className="inline-flex" key={idx}>
78+
<AnnouncementLabel label={label} />
79+
</li>
80+
))}
81+
</ul>
82+
</div>
83+
<Markdown
84+
className="prose text-theme-text-secondary"
85+
markdown={currentItem.description}
86+
/>
87+
</div>
88+
</div>
89+
<div className="flex items-center justify-end gap-2 p-5">
90+
{(currentItem.learn_more_url || currentItem.docs_url) && (
91+
<AnnouncementLinks
92+
docs_url={currentItem.docs_url}
93+
learn_more_url={currentItem.learn_more_url}
94+
slug={currentItem.slug}
95+
size="md"
96+
/>
97+
)}
98+
<Button size="md" onClick={handleNext}>
99+
{isLastPage ? "Got it" : "Next"}
100+
</Button>
101+
</div>
102+
</DialogContent>
103+
</Dialog>
104+
);
105+
}

0 commit comments

Comments
 (0)