mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-06 00:36:16 +02:00
feat: subscribe to release notes
This commit is contained in:
parent
e7336f2a8e
commit
2ecf6aca70
|
|
@ -1,7 +1,7 @@
|
||||||
import { DockerService } from '#services/docker_service';
|
import { DockerService } from '#services/docker_service';
|
||||||
import { SystemService } from '#services/system_service'
|
import { SystemService } from '#services/system_service'
|
||||||
import { SystemUpdateService } from '#services/system_update_service'
|
import { SystemUpdateService } from '#services/system_update_service'
|
||||||
import { affectServiceValidator, installServiceValidator } from '#validators/system';
|
import { affectServiceValidator, installServiceValidator, subscribeToReleaseNotesValidator } from '#validators/system';
|
||||||
import { inject } from '@adonisjs/core'
|
import { inject } from '@adonisjs/core'
|
||||||
import type { HttpContext } from '@adonisjs/core/http'
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
|
||||||
|
|
@ -102,4 +102,10 @@ export default class SystemController {
|
||||||
const logs = this.systemUpdateService.getUpdateLogs();
|
const logs = this.systemUpdateService.getUpdateLogs();
|
||||||
response.send({ logs });
|
response.send({ logs });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async subscribeToReleaseNotes({ request }: HttpContext) {
|
||||||
|
const reqData = await request.validateUsing(subscribeToReleaseNotesValidator);
|
||||||
|
return await this.systemService.subscribeToReleaseNotes(reqData.email);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -226,6 +226,34 @@ export class SystemService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async subscribeToReleaseNotes(email: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
'https://api.projectnomad.us/api/v1/lists/release-notes/subscribe',
|
||||||
|
{ email },
|
||||||
|
{ timeout: 5000 }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Successfully subscribed to release notes',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to subscribe: ${response.statusText}`,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error subscribing to release notes:', error)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to subscribe: ${error instanceof Error ? error.message : error}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the current state of Docker containers against the database records and updates the database accordingly.
|
* Checks the current state of Docker containers against the database records and updates the database accordingly.
|
||||||
* It will mark services as not installed if their corresponding containers do not exist, regardless of their running state.
|
* It will mark services as not installed if their corresponding containers do not exist, regardless of their running state.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,20 @@
|
||||||
import vine from '@vinejs/vine'
|
import vine from '@vinejs/vine'
|
||||||
|
|
||||||
export const installServiceValidator = vine.compile(vine.object({
|
export const installServiceValidator = vine.compile(
|
||||||
service_name: vine.string().trim()
|
vine.object({
|
||||||
}));
|
|
||||||
|
|
||||||
export const affectServiceValidator = vine.compile(vine.object({
|
|
||||||
service_name: vine.string().trim(),
|
service_name: vine.string().trim(),
|
||||||
action: vine.enum(['start', 'stop', 'restart'])
|
})
|
||||||
}));
|
)
|
||||||
|
|
||||||
|
export const affectServiceValidator = vine.compile(
|
||||||
|
vine.object({
|
||||||
|
service_name: vine.string().trim(),
|
||||||
|
action: vine.enum(['start', 'stop', 'restart']),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const subscribeToReleaseNotesValidator = vine.compile(
|
||||||
|
vine.object({
|
||||||
|
email: vine.string().email().trim(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
className?: string;
|
className?: string;
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
inputClassName?: string;
|
inputClassName?: string;
|
||||||
|
containerClassName?: string;
|
||||||
leftIcon?: React.ReactNode;
|
leftIcon?: React.ReactNode;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
|
@ -18,6 +19,7 @@ const Input: React.FC<InputProps> = ({
|
||||||
name,
|
name,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
inputClassName,
|
inputClassName,
|
||||||
|
containerClassName,
|
||||||
leftIcon,
|
leftIcon,
|
||||||
error,
|
error,
|
||||||
required,
|
required,
|
||||||
|
|
@ -31,7 +33,7 @@ const Input: React.FC<InputProps> = ({
|
||||||
>
|
>
|
||||||
{label}{required ? "*" : ""}
|
{label}{required ? "*" : ""}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1.5">
|
<div className={classNames("mt-1.5", containerClassName)}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{leftIcon && (
|
{leftIcon && (
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2">
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2">
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,16 @@ class API {
|
||||||
return response.data
|
return response.data
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async subscribeToReleaseNotes(email: string) {
|
||||||
|
return catchInternal(async () => {
|
||||||
|
const response = await this.client.post<{ success: boolean; message: string }>(
|
||||||
|
'/system/subscribe-release-notes',
|
||||||
|
{ email }
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
})()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new API()
|
export default new API()
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ import { useEffect, useState } from 'react'
|
||||||
import { IconCircleCheck } from '@tabler/icons-react'
|
import { IconCircleCheck } from '@tabler/icons-react'
|
||||||
import { SystemUpdateStatus } from '../../../types/system'
|
import { SystemUpdateStatus } from '../../../types/system'
|
||||||
import api from '~/lib/api'
|
import api from '~/lib/api'
|
||||||
|
import Input from '~/components/inputs/Input'
|
||||||
|
import { useMutation } from '@tanstack/react-query'
|
||||||
|
import { useNotifications } from '~/context/NotificationContext'
|
||||||
|
|
||||||
export default function SystemUpdatePage(props: {
|
export default function SystemUpdatePage(props: {
|
||||||
system: {
|
system: {
|
||||||
|
|
@ -18,11 +21,14 @@ export default function SystemUpdatePage(props: {
|
||||||
currentVersion: string
|
currentVersion: string
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
|
const { addNotification } = useNotifications()
|
||||||
|
|
||||||
const [isUpdating, setIsUpdating] = useState(false)
|
const [isUpdating, setIsUpdating] = useState(false)
|
||||||
const [updateStatus, setUpdateStatus] = useState<SystemUpdateStatus | null>(null)
|
const [updateStatus, setUpdateStatus] = useState<SystemUpdateStatus | null>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [showLogs, setShowLogs] = useState(false)
|
const [showLogs, setShowLogs] = useState(false)
|
||||||
const [logs, setLogs] = useState<string>('')
|
const [logs, setLogs] = useState<string>('')
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isUpdating) return
|
if (!isUpdating) return
|
||||||
|
|
@ -116,10 +122,33 @@ export default function SystemUpdatePage(props: {
|
||||||
if (updateStatus?.stage === 'error')
|
if (updateStatus?.stage === 'error')
|
||||||
return <IconAlertCircle className="h-12 w-12 text-desert-red" />
|
return <IconAlertCircle className="h-12 w-12 text-desert-red" />
|
||||||
if (isUpdating) return <IconRefresh className="h-12 w-12 text-desert-green animate-spin" />
|
if (isUpdating) return <IconRefresh className="h-12 w-12 text-desert-green animate-spin" />
|
||||||
if (props.system.updateAvailable) return <IconArrowBigUpLines className="h-16 w-16 text-desert-green" />
|
if (props.system.updateAvailable)
|
||||||
|
return <IconArrowBigUpLines className="h-16 w-16 text-desert-green" />
|
||||||
return <IconCircleCheck className="h-16 w-16 text-desert-olive" />
|
return <IconCircleCheck className="h-16 w-16 text-desert-olive" />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subscribeToReleaseNotesMutation = useMutation({
|
||||||
|
mutationKey: ['subscribeToReleaseNotes'],
|
||||||
|
mutationFn: (email: string) => api.subscribeToReleaseNotes(email),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
addNotification({ type: 'success', message: 'Successfully subscribed to release notes!' })
|
||||||
|
setEmail('')
|
||||||
|
} else {
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: `Failed to subscribe: ${data?.message || 'Unknown error'}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: `Error subscribing to release notes: ${error.message || 'Unknown error'}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<Head title="System Update" />
|
<Head title="System Update" />
|
||||||
|
|
@ -128,7 +157,8 @@ export default function SystemUpdatePage(props: {
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-4xl font-bold text-desert-green mb-2">System Update</h1>
|
<h1 className="text-4xl font-bold text-desert-green mb-2">System Update</h1>
|
||||||
<p className="text-desert-stone-dark">
|
<p className="text-desert-stone-dark">
|
||||||
Keep your Project N.O.M.A.D. instance up to date with the latest features and improvements.
|
Keep your Project N.O.M.A.D. instance up to date with the latest features and
|
||||||
|
improvements.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -161,9 +191,7 @@ export default function SystemUpdatePage(props: {
|
||||||
{!isUpdating && (
|
{!isUpdating && (
|
||||||
<>
|
<>
|
||||||
<h2 className="text-2xl font-bold text-desert-green mb-2">
|
<h2 className="text-2xl font-bold text-desert-green mb-2">
|
||||||
{props.system.updateAvailable
|
{props.system.updateAvailable ? 'Update Available' : 'System Up to Date'}
|
||||||
? 'Update Available'
|
|
||||||
: 'System Up to Date'}
|
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-desert-stone-dark mb-6">
|
<p className="text-desert-stone-dark mb-6">
|
||||||
{props.system.updateAvailable
|
{props.system.updateAvailable
|
||||||
|
|
@ -305,6 +333,43 @@ export default function SystemUpdatePage(props: {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bg-white rounded-lg border shadow-md overflow-hidden py-6 mt-6">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-center p-8 gap-y-8 md:gap-y-0 gap-x-8">
|
||||||
|
<div>
|
||||||
|
<h2 className="max-w-xl text-lg font-bold text-desert-green sm:text-xl lg:col-span-7">
|
||||||
|
Want to stay updated with the latest from Project N.O.M.A.D.? Subscribe to receive
|
||||||
|
release notes directly to your inbox. Unsubscribe anytime.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex gap-x-3">
|
||||||
|
<Input
|
||||||
|
name="email"
|
||||||
|
label=""
|
||||||
|
type="email"
|
||||||
|
placeholder="Your email address"
|
||||||
|
disabled={false}
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full"
|
||||||
|
containerClassName="!mt-0"
|
||||||
|
/>
|
||||||
|
<StyledButton
|
||||||
|
variant="primary"
|
||||||
|
disabled={!email}
|
||||||
|
onClick={() => subscribeToReleaseNotesMutation.mutateAsync(email)}
|
||||||
|
loading={subscribeToReleaseNotesMutation.isPending}
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</StyledButton>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-sm text-desert-stone-dark">
|
||||||
|
We care about your privacy. Project N.O.M.A.D. will never share your email with
|
||||||
|
third parties or send you spam.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Alert
|
<Alert
|
||||||
type="info"
|
type="info"
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ router
|
||||||
router.post('/services/affect', [SystemController, 'affectService'])
|
router.post('/services/affect', [SystemController, 'affectService'])
|
||||||
router.post('/services/install', [SystemController, 'installService'])
|
router.post('/services/install', [SystemController, 'installService'])
|
||||||
router.post('/services/force-reinstall', [SystemController, 'forceReinstallService'])
|
router.post('/services/force-reinstall', [SystemController, 'forceReinstallService'])
|
||||||
|
router.post('/subscribe-release-notes', [SystemController, 'subscribeToReleaseNotes'])
|
||||||
router.get('/latest-version', [SystemController, 'checkLatestVersion'])
|
router.get('/latest-version', [SystemController, 'checkLatestVersion'])
|
||||||
router.post('/update', [SystemController, 'requestSystemUpdate'])
|
router.post('/update', [SystemController, 'requestSystemUpdate'])
|
||||||
router.get('/update/status', [SystemController, 'getSystemUpdateStatus'])
|
router.get('/update/status', [SystemController, 'getSystemUpdateStatus'])
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user