mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-26 10:31:17 +00:00
Add v6 RC announcement surfaces to v5
This commit is contained in:
parent
dfbe2eb873
commit
a24af45c67
6 changed files with 303 additions and 0 deletions
|
|
@ -35,6 +35,7 @@ import { updateStore } from './stores/updates';
|
|||
import { UpdateBanner } from './components/UpdateBanner';
|
||||
import { DemoBanner } from './components/DemoBanner';
|
||||
import { GitHubStarBanner } from './components/GitHubStarBanner';
|
||||
import { ReleaseAnnouncementBanner } from './components/ReleaseAnnouncementBanner';
|
||||
import { createTooltipSystem } from './components/shared/Tooltip';
|
||||
import type { State, Alert } from '@/types/api';
|
||||
import { ProxmoxIcon } from '@/components/icons/ProxmoxIcon';
|
||||
|
|
@ -922,6 +923,10 @@ function App() {
|
|||
<SecurityWarning />
|
||||
<DemoBanner />
|
||||
<UpdateBanner />
|
||||
<ReleaseAnnouncementBanner
|
||||
versionInfo={versionInfo}
|
||||
securityStatus={securityStatus}
|
||||
/>
|
||||
<GitHubStarBanner />
|
||||
<GlobalUpdateProgressWatcher />
|
||||
</Show>
|
||||
|
|
|
|||
115
frontend-modern/src/components/ReleaseAnnouncementBanner.tsx
Normal file
115
frontend-modern/src/components/ReleaseAnnouncementBanner.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import type { Accessor } from 'solid-js';
|
||||
import { createMemo, Show } from 'solid-js';
|
||||
import { useLocation } from '@solidjs/router';
|
||||
import type { VersionInfo } from '@/api/updates';
|
||||
import type { SecurityStatus } from '@/types/config';
|
||||
import {
|
||||
V6_RC_ANNOUNCEMENT,
|
||||
shouldShowV6RcAnnouncement,
|
||||
} from '@/constants/releaseAnnouncements';
|
||||
import { createLocalStorageStringSignal, STORAGE_KEYS } from '@/utils/localStorage';
|
||||
import FlaskConicalIcon from 'lucide-solid/icons/flask-conical';
|
||||
import ExternalLinkIcon from 'lucide-solid/icons/external-link';
|
||||
import XIcon from 'lucide-solid/icons/x';
|
||||
|
||||
interface ReleaseAnnouncementBannerProps {
|
||||
versionInfo: Accessor<VersionInfo | null>;
|
||||
securityStatus: Accessor<SecurityStatus | null>;
|
||||
}
|
||||
|
||||
export function ReleaseAnnouncementBanner(props: ReleaseAnnouncementBannerProps) {
|
||||
const location = useLocation();
|
||||
const [dismissedAnnouncementId, setDismissedAnnouncementId] =
|
||||
createLocalStorageStringSignal(STORAGE_KEYS.RELEASE_ANNOUNCEMENT_DISMISSED, '');
|
||||
|
||||
const isDismissed = createMemo(
|
||||
() => dismissedAnnouncementId() === V6_RC_ANNOUNCEMENT.id,
|
||||
);
|
||||
|
||||
const shouldShow = createMemo(() => {
|
||||
if (isDismissed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return shouldShowV6RcAnnouncement({
|
||||
version: props.versionInfo()?.version,
|
||||
pathname: location.pathname,
|
||||
securityStatus: props.securityStatus(),
|
||||
});
|
||||
});
|
||||
|
||||
const dismiss = () => {
|
||||
setDismissedAnnouncementId(V6_RC_ANNOUNCEMENT.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Show when={shouldShow()}>
|
||||
<div class="border-b border-emerald-200 bg-emerald-50 text-emerald-950 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100">
|
||||
<div class="px-4 py-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex min-w-0 flex-1 items-start gap-3">
|
||||
<div class="mt-0.5 rounded-full bg-emerald-100 p-2 text-emerald-700 dark:bg-emerald-900/70 dark:text-emerald-200">
|
||||
<FlaskConicalIcon class="h-4 w-4" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold">Pulse v6 RC testing</span>
|
||||
<span class="rounded-full bg-emerald-100 px-2 py-0.5 text-[11px] font-medium text-emerald-800 dark:bg-emerald-900/70 dark:text-emerald-200">
|
||||
{V6_RC_ANNOUNCEMENT.tag}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-1 text-sm leading-relaxed text-emerald-900/85 dark:text-emerald-100/85">
|
||||
<code class="rounded bg-emerald-100 px-1 py-0.5 text-[12px] dark:bg-emerald-900/70">
|
||||
5.1.x
|
||||
</code>{' '}
|
||||
remains the current stable line. Pulse v6 changes the runtime,
|
||||
upgrade path, navigation, and product model substantially. If you rely on
|
||||
Pulse today, test v6 in a staging or non-production environment and report
|
||||
any issues before the stable cut.
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<a
|
||||
href={V6_RC_ANNOUNCEMENT.changelogUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 rounded-md bg-emerald-700 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-emerald-800 dark:bg-emerald-600 dark:hover:bg-emerald-500"
|
||||
>
|
||||
Read v6 changelog
|
||||
<ExternalLinkIcon class="h-3.5 w-3.5" />
|
||||
</a>
|
||||
<a
|
||||
href={V6_RC_ANNOUNCEMENT.releaseUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-emerald-300 bg-white px-3 py-1.5 text-sm font-medium text-emerald-900 transition-colors hover:bg-emerald-100 dark:border-emerald-800 dark:bg-emerald-950/20 dark:text-emerald-100 dark:hover:bg-emerald-900/40"
|
||||
>
|
||||
View v6 RC
|
||||
<ExternalLinkIcon class="h-3.5 w-3.5" />
|
||||
</a>
|
||||
<a
|
||||
href={V6_RC_ANNOUNCEMENT.demoUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-transparent px-2 py-1.5 text-sm font-medium text-emerald-800 transition-colors hover:bg-emerald-100 dark:text-emerald-200 dark:hover:bg-emerald-900/30"
|
||||
>
|
||||
Open demo
|
||||
<ExternalLinkIcon class="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismiss}
|
||||
class="rounded-md p-1 text-emerald-700 transition-colors hover:bg-emerald-100 hover:text-emerald-900 dark:text-emerald-300 dark:hover:bg-emerald-900/50 dark:hover:text-emerald-100"
|
||||
title="Dismiss"
|
||||
aria-label="Dismiss v6 RC announcement"
|
||||
>
|
||||
<XIcon class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,10 @@ import ArrowRight from 'lucide-solid/icons/arrow-right';
|
|||
import Package from 'lucide-solid/icons/package';
|
||||
import Download from 'lucide-solid/icons/download';
|
||||
import type { UpdateInfo, VersionInfo, UpdatePlan } from '@/api/updates';
|
||||
import {
|
||||
V6_RC_ANNOUNCEMENT,
|
||||
isV5ReleaseLine,
|
||||
} from '@/constants/releaseAnnouncements';
|
||||
|
||||
interface UpdatesSettingsPanelProps {
|
||||
versionInfo: Accessor<VersionInfo | null>;
|
||||
|
|
@ -39,6 +43,60 @@ export const UpdatesSettingsPanel: Component<UpdatesSettingsPanelProps> = (props
|
|||
>
|
||||
<section class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<Show when={isV5ReleaseLine(props.versionInfo()?.version)}>
|
||||
<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-900/70 dark:bg-emerald-950/30">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold text-emerald-900 dark:text-emerald-100">
|
||||
Pulse v6 RC testing
|
||||
</span>
|
||||
<span class="inline-flex items-center rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-medium text-emerald-800 dark:bg-emerald-900/70 dark:text-emerald-200">
|
||||
{V6_RC_ANNOUNCEMENT.tag}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-emerald-900/85 dark:text-emerald-100/85">
|
||||
<code class="rounded bg-emerald-100 px-1 py-0.5 text-[12px] dark:bg-emerald-900/70">
|
||||
5.1.x
|
||||
</code>{' '}
|
||||
remains the stable line. If you can, test v6 in a staging or
|
||||
non-production environment and use the changelog before upgrading so the
|
||||
move from v5 is deliberate rather than guesswork.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a
|
||||
href={V6_RC_ANNOUNCEMENT.changelogUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 rounded-lg bg-emerald-700 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-800 dark:bg-emerald-600 dark:hover:bg-emerald-500"
|
||||
>
|
||||
Read v6 changelog
|
||||
<Download class="w-4 h-4" />
|
||||
</a>
|
||||
<a
|
||||
href={V6_RC_ANNOUNCEMENT.releaseUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 rounded-lg border border-emerald-300 px-3 py-2 text-sm font-medium text-emerald-900 transition-colors hover:bg-emerald-100 dark:border-emerald-800 dark:text-emerald-100 dark:hover:bg-emerald-900/40"
|
||||
>
|
||||
View v6 RC
|
||||
<ArrowRight class="w-4 h-4" />
|
||||
</a>
|
||||
<a
|
||||
href={V6_RC_ANNOUNCEMENT.demoUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 rounded-lg px-3 py-2 text-sm font-medium text-emerald-800 transition-colors hover:bg-emerald-100 dark:text-emerald-200 dark:hover:bg-emerald-900/30"
|
||||
>
|
||||
Open demo
|
||||
<ArrowRight class="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Version Status Section */}
|
||||
<div class="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
{/* Version Grid */}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type { SecurityStatus } from '@/types/config';
|
||||
import {
|
||||
canSeeAdminReleaseAnnouncement,
|
||||
isV5ReleaseLine,
|
||||
shouldShowV6RcAnnouncement,
|
||||
} from '@/constants/releaseAnnouncements';
|
||||
|
||||
describe('releaseAnnouncements', () => {
|
||||
it('detects the v5 release line from semver strings', () => {
|
||||
expect(isV5ReleaseLine('v5.1.27')).toBe(true);
|
||||
expect(isV5ReleaseLine('5.1.28')).toBe(true);
|
||||
expect(isV5ReleaseLine('v6.0.0-rc.1')).toBe(false);
|
||||
expect(isV5ReleaseLine('')).toBe(false);
|
||||
});
|
||||
|
||||
it('allows the announcement for non-proxy-auth sessions', () => {
|
||||
const status = {
|
||||
hasProxyAuth: false,
|
||||
proxyAuthIsAdmin: false,
|
||||
} as SecurityStatus;
|
||||
|
||||
expect(canSeeAdminReleaseAnnouncement(status)).toBe(true);
|
||||
});
|
||||
|
||||
it('blocks the announcement for non-admin proxy-auth sessions', () => {
|
||||
const status = {
|
||||
hasProxyAuth: true,
|
||||
proxyAuthIsAdmin: false,
|
||||
} as SecurityStatus;
|
||||
|
||||
expect(canSeeAdminReleaseAnnouncement(status)).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the announcement only on the supported v5 surfaces', () => {
|
||||
expect(
|
||||
shouldShowV6RcAnnouncement({
|
||||
version: 'v5.1.27',
|
||||
pathname: '/proxmox/overview',
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
shouldShowV6RcAnnouncement({
|
||||
version: 'v5.1.27',
|
||||
pathname: '/settings/updates',
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
shouldShowV6RcAnnouncement({
|
||||
version: 'v5.1.27',
|
||||
pathname: '/alerts/overview',
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
shouldShowV6RcAnnouncement({
|
||||
version: 'v6.0.0-rc.1',
|
||||
pathname: '/settings/updates',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
60
frontend-modern/src/constants/releaseAnnouncements.ts
Normal file
60
frontend-modern/src/constants/releaseAnnouncements.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import type { SecurityStatus } from '@/types/config';
|
||||
|
||||
export const V6_RC_ANNOUNCEMENT = {
|
||||
id: 'v6-rc-testing-v6.0.0-rc.1',
|
||||
tag: 'v6.0.0-rc.1',
|
||||
releaseUrl: 'https://github.com/rcourtman/Pulse/releases/tag/v6.0.0-rc.1',
|
||||
changelogUrl: 'https://github.com/rcourtman/Pulse/blob/pulse/v6-release/docs/releases/V6_CHANGELOG.md',
|
||||
demoUrl: 'https://v6-demo.pulserelay.pro',
|
||||
} as const;
|
||||
|
||||
function parseMajorVersion(version?: string | null): number | null {
|
||||
const match = String(version || '').trim().match(/^v?(\d+)/i);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const major = Number(match[1]);
|
||||
return Number.isFinite(major) ? major : null;
|
||||
}
|
||||
|
||||
export function isV5ReleaseLine(version?: string | null): boolean {
|
||||
return parseMajorVersion(version) === 5;
|
||||
}
|
||||
|
||||
export function canSeeAdminReleaseAnnouncement(
|
||||
securityStatus?: SecurityStatus | null,
|
||||
): boolean {
|
||||
if (!securityStatus) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!securityStatus.hasProxyAuth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return securityStatus.proxyAuthIsAdmin === true;
|
||||
}
|
||||
|
||||
export function shouldShowV6RcAnnouncement(opts: {
|
||||
version?: string | null;
|
||||
pathname?: string | null;
|
||||
securityStatus?: SecurityStatus | null;
|
||||
}): boolean {
|
||||
if (!isV5ReleaseLine(opts.version)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!canSeeAdminReleaseAnnouncement(opts.securityStatus)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const path = opts.pathname || '';
|
||||
return (
|
||||
path === '/' ||
|
||||
path === '/proxmox' ||
|
||||
path === '/proxmox/overview' ||
|
||||
path === '/settings' ||
|
||||
path.startsWith('/settings/')
|
||||
);
|
||||
}
|
||||
|
|
@ -164,6 +164,7 @@ export const STORAGE_KEYS = {
|
|||
|
||||
// Feature discovery
|
||||
DISMISSED_FEATURE_TIPS: 'pulse-dismissed-feature-tips',
|
||||
RELEASE_ANNOUNCEMENT_DISMISSED: 'pulse-release-announcement-dismissed',
|
||||
|
||||
// GitHub star prompt
|
||||
GITHUB_STAR_DISMISSED: 'pulse-github-star-dismissed',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue