Skip to content
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
9 changes: 8 additions & 1 deletion web/messages/en/modal.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,5 +203,12 @@
"modal_delete_api_token_success": "API token deleted",
"modal_delete_api_token_error": "Failed to delete API token",
"modal_delete_authorized_app_success": "Application removed",
"modal_delete_authorized_app_error": "Failed to remove application"
"modal_delete_authorized_app_error": "Failed to remove application",
"modal_app_update_title": "New update available",
"modal_app_update_critical_badge": "Critical",
"modal_app_update_full_changelog": "Full Changelog",
"modal_app_update_go_to_release": "Go to release page",
"modal_app_update_dismiss": "Dismiss",
"modal_app_update_snackbar_message": "New version {version} is available",
"modal_app_update_snackbar_action": "What's new"
}
2 changes: 2 additions & 0 deletions web/src/routes/_authorized.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
import { DisplayListModal } from '../shared/components/DisplayListModal/DisplayListModal';
import { AppUpdateModal } from '../shared/components/modals/AppUpdateModal/AppUpdateModal';
import { ConfirmActionModal } from '../shared/components/modals/ConfirmActionModal/ConfirmActionModal';
import { LicenseExpiredModal } from '../shared/components/modals/license/LicenseExpiredModal/LicenseExpiredModal';
import { LicenseLimitConflictModal } from '../shared/components/modals/license/LicenseLimitConflictModal/LicenseLimitConflictModal';
Expand Down Expand Up @@ -64,6 +65,7 @@ function RouteComponent() {
<DisplayListModal />
<SelectionModal />
<ConfirmActionModal />
<AppUpdateModal />
</AppInfoProvider>
</AppUserProvider>
);
Expand Down
2 changes: 2 additions & 0 deletions web/src/shared/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import type {
StartEnrollmentResponse,
TestDirectorySyncResponse,
TotpInitResponse,
UpdateInfo,
UploadCARequest,
User,
UserChangePasswordRequest,
Expand Down Expand Up @@ -175,6 +176,7 @@ const api = {
},
app: {
info: () => client.get<ApplicationInfo>('/info'),
updates: () => client.get<UpdateInfo | null>('/updates'),
},
user: {
addUser: (data: AddUserRequest) => client.post<User>('/user', data),
Expand Down
9 changes: 9 additions & 0 deletions web/src/shared/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,15 @@ export interface ApplicationInfo {
ldap_info: LdapInfo;
}

export interface UpdateInfo {
version: string;
release_date: string;
release_notes_url: string;
update_url: string;
critical: boolean;
notes: string;
}

export interface WebauthnRegisterStartResponse {
publicKey: PublicKeyCredentialCreationOptionsJSON;
}
Expand Down
134 changes: 134 additions & 0 deletions web/src/shared/components/modals/AppUpdateModal/AppUpdateModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import './style.scss';

import { useEffect, useMemo, useState } from 'react';

import { m } from '../../../../paraglide/messages';
import { DISMISSED_UPDATE_KEY } from '../../../constants';
import { AppText } from '../../../defguard-ui/components/AppText/AppText';
import { Badge } from '../../../defguard-ui/components/Badge/Badge';
import { Button } from '../../../defguard-ui/components/Button/Button';
import { Divider } from '../../../defguard-ui/components/Divider/Divider';
import { Icon } from '../../../defguard-ui/components/Icon';
import { ModalFoundation } from '../../../defguard-ui/components/ModalFoundation/ModalFoundation';
import { RenderMarkdown } from '../../../defguard-ui/components/RenderMarkdown/RenderMarkdown';
import { TextStyle, ThemeSpacing, ThemeVariable } from '../../../defguard-ui/types';
import { isPresent } from '../../../defguard-ui/utils/isPresent';
import {
closeModal,
subscribeCloseModal,
subscribeOpenModal,
} from '../../../hooks/modalControls/modalsSubjects';
import { ModalName } from '../../../hooks/modalControls/modalTypes';
import type { OpenAppUpdateModal } from '../../../hooks/modalControls/types';
import { Controls } from '../../Controls/Controls';
import updateImage from './update-image.png';

const modalNameKey = ModalName.AppUpdate;

type ModalData = OpenAppUpdateModal;

export const AppUpdateModal = () => {
const [isOpen, setOpen] = useState(false);
const [modalData, setModalData] = useState<ModalData | null>(null);

useEffect(() => {
const openSub = subscribeOpenModal(modalNameKey, (data) => {
setModalData(data);
setOpen(true);
});
const closeSub = subscribeCloseModal(modalNameKey, () => setOpen(false));
return () => {
openSub.unsubscribe();
closeSub.unsubscribe();
};
}, []);

return (
<ModalFoundation
id="app-update-modal"
contentClassName="app-update-modal"
isOpen={isOpen}
afterClose={() => {
setModalData(null);
}}
>
<div className="tracks">
<div className="content-track">
{isPresent(modalData) && <ModalContent data={modalData} />}
</div>
<div className="media-track">
<img
src={updateImage}
id="update-image"
width={842}
height={799}
style={{ top: -30, left: -220 }}
/>
</div>
</div>
</ModalFoundation>
);
};

const ModalContent = ({ data }: { data: ModalData }) => {
const handleDismiss = () => {
localStorage.setItem(DISMISSED_UPDATE_KEY, data.version);
closeModal(modalNameKey);
};

const { subtitle, body } = useMemo(() => {
const trimmed = data.notes.trim();
const firstBlank = trimmed.search(/\n\s*\n/);
return {
subtitle: firstBlank === -1 ? trimmed : trimmed.slice(0, firstBlank).trim(),
body: firstBlank === -1 ? '' : trimmed.slice(firstBlank).trim(),
};
}, [data.notes]);

return (
<>
<div className="header-section">
{data.critical && (
<Badge
text={m.modal_app_update_critical_badge()}
variant="critical"
showIcon
icon="status-important"
/>
)}
<AppText font={TextStyle.TTitleH1} color={ThemeVariable.FgDefault}>
{m.modal_app_update_title()}
</AppText>
<RenderMarkdown content={subtitle} />
</div>
<Divider spacing={ThemeSpacing.Lg} />
<RenderMarkdown content={body} />
<Divider spacing={ThemeSpacing.Lg} />
<a
className="changelog-link"
href={data.release_notes_url}
target="_blank"
rel="noopener noreferrer"
>
<Icon icon="arrow-big" size={20} staticColor={ThemeVariable.FgAction} />
<AppText font={TextStyle.TBodySm400} color={ThemeVariable.FgAction}>
{m.modal_app_update_full_changelog()}
</AppText>
</a>
<Controls>
<a href={data.update_url} target="_blank" rel="noopener noreferrer">
<Button
text={m.modal_app_update_go_to_release()}
variant="primary"
iconRight="open-in-new-window"
/>
</a>
<Button
text={m.modal_app_update_dismiss()}
variant="secondary"
onClick={handleDismiss}
/>
</Controls>
</>
);
};
108 changes: 108 additions & 0 deletions web/src/shared/components/modals/AppUpdateModal/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#modals-root .app-update-modal {
background-color: var(--bg-default);
border-radius: var(--radius-xxl);
width: 100%;
max-width: 1110px;
overflow: hidden;

& > .tracks {
display: grid;
grid-template-columns: 667fr 443fr;
height: 657px;

& > .content-track {
box-sizing: border-box;
padding: var(--spacing-4xl);
display: flex;
flex-flow: column;
background-color: var(--bg-default);
overflow: hidden;

.header-section {
display: flex;
flex-flow: column;
row-gap: var(--spacing-sm);

.markdown-render p {
font: var(--t-body-primary-600);
}
}

.changelog-link {
text-decoration: none;
display: flex;
flex-flow: row;
align-items: center;
column-gap: var(--spacing-sm);
width: fit-content;

p {
text-decoration: underline;
}
}

.controls {
margin-top: auto;
padding-top: var(--spacing-2xl);
}

& > .markdown-render {
overflow-y: auto;
flex: 1 1 0;
min-height: 0;
}

.markdown-render {
display: flex;
flex-flow: column;
row-gap: var(--spacing-sm);

p {
font: var(--t-body-sm-500);
color: var(--fg-default);
}

span,
li {
font: var(--t-body-sm-400);
color: var(--fg-faded);
}

a {
color: var(--fg-action);
text-decoration: underline;
}

ul {
display: flex;
flex-flow: column;
row-gap: var(--spacing-xs);
padding-left: var(--spacing-lg);
}

h1,
h2,
h3 {
font: var(--t-body-sm-500);
color: var(--fg-default);
}

h2 {
padding-top: var(--spacing-lg);
margin-top: var(--spacing-lg);
border-top: 1px solid var(--bg-active);
}
}
}

& > .media-track {
position: relative;
overflow: hidden;
background: linear-gradient(157deg, #e6e6ff -2.15%, #8cadff 102.56%);

img {
position: absolute;
}
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions web/src/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ export const licenseGracePeriodDays = 14;
export const edgeDefaultGrpcPort = 50051;

export const gatewayDefaultGrpcPort = 50066;

export const DISMISSED_UPDATE_KEY = 'dismissed-update-version';
2 changes: 1 addition & 1 deletion web/src/shared/defguard-ui
6 changes: 6 additions & 0 deletions web/src/shared/hooks/modalControls/modalTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
OpenAddApiTokenModal,
OpenAddLocationModal,
OpenAddNetworkDeviceModal,
OpenAppUpdateModal,
OpenAssignUserDeviceIPModal,
OpenAssignUserIPModal,
OpenAssignUsersToGroupsModal,
Expand Down Expand Up @@ -67,6 +68,7 @@ export const ModalName = {
AssignUserIP: 'assignUserIP',
AssignUserDeviceIP: 'assignUserDeviceIP',
ConfirmAction: 'confirmAction',
AppUpdate: 'appUpdate',
} as const;

export type ModalNameValue = (typeof ModalName)[keyof typeof ModalName];
Expand Down Expand Up @@ -216,6 +218,10 @@ const modalOpenArgsSchema = z.discriminatedUnion('name', [
name: z.literal(ModalName.ConfirmAction),
data: z.custom<OpenConfirmActionModal>(),
}),
z.object({
name: z.literal(ModalName.AppUpdate),
data: z.custom<OpenAppUpdateModal>(),
}),
]);

export type ModalOpenEvent = z.infer<typeof modalOpenArgsSchema>;
3 changes: 3 additions & 0 deletions web/src/shared/hooks/modalControls/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
NetworkLocation,
OpenIdClient,
StartEnrollmentResponse,
UpdateInfo,
User,
Webhook,
} from '../../api/types';
Expand Down Expand Up @@ -165,3 +166,5 @@ export interface OpenAssignUserDeviceIPModal {
username: string;
locationData: DeviceLocationIpsResponse;
}

export interface OpenAppUpdateModal extends UpdateInfo {}
Loading
Loading