feat(web): activation modal steps, updated copy (#1079)

* feat(stepper): add shadcn stepper components

* chore(serverState): remove partnerLogo property from server state configuration

* refactor(web): modal add subfooter slot

- adds ability to display content below the modal's content box

* feat(modal): add ActivationSteps component to subFooter slot in WelcomeModal and ActivationModal

* refactor: improve activation modal buttons responsiveness

* refactor: update activation flow messaging and UI

* feat: web/deploy-dev.sh add dynamic web component JS file whitelisting in auth-request.php

* fix: remove test UTM parameters from Unraid docs links in activation modal

* refactor: improve konami code handling and add type safety to activation steps

* chore: remove extra semicolon in serverState.ts

* Apply suggestions from code review

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Zack Spear
2025-01-29 11:08:23 -08:00
committed by GitHub
parent 8481c9a9fb
commit d8a5b1711a
17 changed files with 470 additions and 50 deletions

View File

@@ -60,7 +60,7 @@ fi
if [ "$deploy" = "yes" ]; then if [ "$deploy" = "yes" ]; then
cd web || exit cd web || exit
npm run deploy-wc:dev npm run deploy-to-unraid:dev
elif [ "$deploy" = "build" ]; then elif [ "$deploy" = "build" ]; then
cd web || exit cd web || exit
npm run build:dev npm run build:dev

View File

@@ -80,6 +80,7 @@ class WebComponentTranslations
'<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>' => '<p>' . _('Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.') . '</p>', '<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>' => '<p>' . _('Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.') . '</p>',
'A Trial key provides all the functionality of an Unleashed Registration key' => _('A Trial key provides all the functionality of an Unleashed Registration key'), 'A Trial key provides all the functionality of an Unleashed Registration key' => _('A Trial key provides all the functionality of an Unleashed Registration key'),
'Acklowledge that you have made a Flash Backup to enable this action' => _('Acklowledge that you have made a Flash Backup to enable this action'), 'Acklowledge that you have made a Flash Backup to enable this action' => _('Acklowledge that you have made a Flash Backup to enable this action'),
'Activate License' => _('Activate License'),
'Activate Now' => _('Activate Now'), 'Activate Now' => _('Activate Now'),
'ago' => _('ago'), 'ago' => _('ago'),
'All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.' => _('All you need is an active internet connection, an Unraid.net account, and the Connect plugin.') . ' ' . _('Get started by installing the plugin.'), 'All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.' => _('All you need is an active internet connection, an Unraid.net account, and the Connect plugin.') . ' ' . _('Get started by installing the plugin.'),
@@ -119,11 +120,14 @@ class WebComponentTranslations
'Copy your Key URL: {0}' => sprintf(_('Copy your Key URL: %s'), '{0}'), 'Copy your Key URL: {0}' => sprintf(_('Copy your Key URL: %s'), '{0}'),
'Create Flash Backup' => _('Create Flash Backup'), 'Create Flash Backup' => _('Create Flash Backup'),
'Create a password' => _('Create a password'), 'Create a password' => _('Create a password'),
'Create an Unraid.net account and activate your key' => _('Create an Unraid.net account and activate your key'),
'Create Device Password' => _('Create Device Password'),
'Current Version {0}' => sprintf(_('Current Version %s'), '{0}'), 'Current Version {0}' => sprintf(_('Current Version %s'), '{0}'),
'Current Version: Unraid {0}' => sprintf(_('Current Version: Unraid %s'), '{0}'), 'Current Version: Unraid {0}' => sprintf(_('Current Version: Unraid %s'), '{0}'),
'Customizable Dashboard Tiles' => _('Customizable Dashboard Tiles'), 'Customizable Dashboard Tiles' => _('Customizable Dashboard Tiles'),
'day' => sprintf(_('%s day'), '{n}') . ' | ' . sprintf(_('%s days'), '{n}'), 'day' => sprintf(_('%s day'), '{n}') . ' | ' . sprintf(_('%s days'), '{n}'),
'Deep Linking' => _('Deep Linking'), 'Deep Linking' => _('Deep Linking'),
'Device is ready to configure' => _('Device is ready to configure'),
'DNS issue, unable to resolve wanip4.unraid.net' => _('DNS issue, unable to resolve wanip4.unraid.net'), 'DNS issue, unable to resolve wanip4.unraid.net' => _('DNS issue, unable to resolve wanip4.unraid.net'),
'Downgrade Unraid OS to {0}' => sprintf(_('Downgrade Unraid OS to %s'), '{0}'), 'Downgrade Unraid OS to {0}' => sprintf(_('Downgrade Unraid OS to %s'), '{0}'),
'Downgrade Unraid OS' => _('Downgrade Unraid OS'), 'Downgrade Unraid OS' => _('Downgrade Unraid OS'),
@@ -206,7 +210,7 @@ class WebComponentTranslations
'Learn more and link your key to your account' => _('Learn more and link your key to your account'), 'Learn more and link your key to your account' => _('Learn more and link your key to your account'),
'Learn More' => _('Learn More'), 'Learn More' => _('Learn More'),
'Learn more' => _('Learn more'), 'Learn more' => _('Learn more'),
'Let\'s activate your Unraid license!' => _('Let\'s activate your Unraid license!'), 'Let\'s activate your Unraid OS License' => _('Let\'s activate your Unraid OS License'),
'Let\'s Unleash your Hardware!' => _('Let\'s Unleash your Hardware!'), 'Let\'s Unleash your Hardware!' => _('Let\'s Unleash your Hardware!'),
'License key actions' => _('License key actions'), 'License key actions' => _('License key actions'),
'License key type' => _('License key type'), 'License key type' => _('License key type'),
@@ -286,6 +290,7 @@ class WebComponentTranslations
'Requires the local unraid-api to be running successfully' => _('Requires the local unraid-api to be running successfully'), 'Requires the local unraid-api to be running successfully' => _('Requires the local unraid-api to be running successfully'),
'Restarting unraid-api…' => _('Restarting unraid-api…'), 'Restarting unraid-api…' => _('Restarting unraid-api…'),
'second' => sprintf(_('%s second'), '{n}') . ' | ' . sprintf(_('%s seconds'), '{n}'), 'second' => sprintf(_('%s second'), '{n}') . ' | ' . sprintf(_('%s seconds'), '{n}'),
'Secure your device' => _('Secure your device'),
'Server Up Since {0}' => sprintf(_('Server Up Since %s'), '{0}'), 'Server Up Since {0}' => sprintf(_('Server Up Since %s'), '{0}'),
'Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.' => _('Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI.') . ' ' . _('Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.'), 'Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.' => _('Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI.') . ' ' . _('Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.'),
'Set custom server tiles how you like and automatically display your server\'s banner image on your Connect Dashboard.' => _('Set custom server tiles how you like and automatically display your server\'s banner image on your Connect Dashboard.'), 'Set custom server tiles how you like and automatically display your server\'s banner image on your Connect Dashboard.' => _('Set custom server tiles how you like and automatically display your server\'s banner image on your Connect Dashboard.'),
@@ -307,7 +312,7 @@ class WebComponentTranslations
'SSL certificates for unraid.net deprecated' => _('SSL certificates for unraid.net deprecated'), 'SSL certificates for unraid.net deprecated' => _('SSL certificates for unraid.net deprecated'),
'Stale Server' => _('Stale Server'), 'Stale Server' => _('Stale Server'),
'Stale' => _('Stale'), 'Stale' => _('Stale'),
'Start by creating an Unraid.net account — this will let you manage your license and access support. Once that\'s done, we\'ll guide you through a quick checkout process to register your license and install your key.' => _('Start by creating an Unraid.net account — this will let you manage your license and access support.') . ' ' . _('Once that\'s done, we\'ll guide you through a quick checkout process to register your license and install your key.'), 'On the following screen, your license will be activated. You\'ll then create an Unraid.net Account to manage your license going forward.' => _('On the following screen, your license will be activated.') . ' ' . _('You\'ll then create an Unraid.net Account to manage your license going forward.'),
'Start Free 30 Day Trial' => _('Start Free 30 Day Trial'), 'Start Free 30 Day Trial' => _('Start Free 30 Day Trial'),
'Starting your free 30 day trial' => _('Starting your free 30 day trial'), 'Starting your free 30 day trial' => _('Starting your free 30 day trial'),
'Success!' => _('Success!'), 'Success!' => _('Success!'),

View File

@@ -1,4 +1,3 @@
;
// import dayjs, { extend } from 'dayjs'; // import dayjs, { extend } from 'dayjs';
// import customParseFormat from 'dayjs/plugin/customParseFormat'; // import customParseFormat from 'dayjs/plugin/customParseFormat';
// import relativeTime from 'dayjs/plugin/relativeTime'; // import relativeTime from 'dayjs/plugin/relativeTime';
@@ -11,7 +10,6 @@ import type { Server, ServerState
// ServerUpdateOsResponse, // ServerUpdateOsResponse,
} from '~/types/server'; } from '~/types/server';
// dayjs plugins // dayjs plugins
// extend(customParseFormat); // extend(customParseFormat);
// extend(relativeTime); // extend(relativeTime);

View File

@@ -19,10 +19,10 @@ const activationCodeStore = useActivationCodeStore();
const { partnerLogo, showActivationModal } = storeToRefs(activationCodeStore); const { partnerLogo, showActivationModal } = storeToRefs(activationCodeStore);
const purchaseStore = usePurchaseStore(); const purchaseStore = usePurchaseStore();
const title = computed<string>(() => props.t("Let's activate your Unraid license!")); const title = computed<string>(() => props.t("Let's activate your Unraid OS License"));
const description = computed<string>(() => const description = computed<string>(() =>
props.t( props.t(
`Start by creating an Unraid.net account — this will let you manage your license and access support. Once that's done, we'll guide you through a quick checkout process to register your license and install your key.` `On the following screen, your license will be activated. Youll then create an Unraid.net Account to manage your license going forward.`
) )
); );
const docsButtons = computed<ButtonProps[]>(() => { const docsButtons = computed<ButtonProps[]>(() => {
@@ -47,40 +47,41 @@ const docsButtons = computed<ButtonProps[]>(() => {
}); });
/** /**
* Listen for a key sequence to close the modal * Listen for konami code sequence to close the modal
* @todo - temporary solution until we have a better way to handle this
*/ */
const keySequence = [
"ArrowUp",
"ArrowUp",
"ArrowDown",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"ArrowLeft",
"ArrowRight",
"b",
"a",
];
let sequenceIndex = 0;
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === keySequence[sequenceIndex]) {
sequenceIndex++;
} else {
sequenceIndex = 0;
}
if (sequenceIndex === keySequence.length) {
activationCodeStore.setActivationModalHidden(true);
window.location.href = "/Tools/Registration";
}
};
onMounted(() => { onMounted(() => {
const keySequence = [ window.addEventListener("keydown", handleKeydown);
"ArrowUp",
"ArrowUp",
"ArrowDown",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"ArrowLeft",
"ArrowRight",
"b",
"a",
];
let sequenceIndex = 0;
window.addEventListener("keydown", (event) => {
if (event.key === keySequence[sequenceIndex]) {
sequenceIndex++;
} else {
sequenceIndex = 0;
}
if (sequenceIndex === keySequence.length) {
activationCodeStore.setActivationModalHidden(true);
window.location.href = "/Tools/Registration";
}
});
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("keydown", () => {}); window.removeEventListener("keydown", handleKeydown);
}); });
</script> </script>
@@ -103,11 +104,6 @@ onUnmounted(() => {
<ActivationPartnerLogo /> <ActivationPartnerLogo />
</template> </template>
<template #main>
<div class="flex justify-center gap-4 mx-auto w-full">
<BrandButton v-for="button in docsButtons" :key="button.text" v-bind="button" />
</div>
</template>
<template #footer> <template #footer>
<div class="w-full flex gap-8px justify-center mx-auto"> <div class="w-full flex gap-8px justify-center mx-auto">
<BrandButton <BrandButton
@@ -117,5 +113,15 @@ onUnmounted(() => {
/> />
</div> </div>
</template> </template>
<template #subFooter>
<div class="flex flex-col gap-6">
<ActivationSteps :active-step="2" class="hidden sm:flex mt-6" />
<div class="flex flex-col sm:flex-row justify-center gap-4 mx-auto w-full">
<BrandButton v-for="button in docsButtons" :key="button.text" v-bind="button" />
</div>
</div>
</template>
</Modal> </Modal>
</template> </template>

View File

@@ -0,0 +1,119 @@
<script setup lang="ts">
import { Button } from "@/components/shadcn/button";
import {
Stepper,
StepperDescription,
StepperItem,
StepperSeparator,
StepperTitle,
StepperTrigger,
} from "@/components/shadcn/stepper";
import { CheckIcon, KeyIcon, ServerStackIcon } from "@heroicons/vue/24/outline";
import {
KeyIcon as KeyIconSolid,
LockClosedIcon,
ServerStackIcon as ServerStackIconSolid,
} from "@heroicons/vue/24/solid";
import type { Component } from 'vue'
type StepState = "inactive" | "active" | "completed";
withDefaults(defineProps<{
activeStep: number
}>(), {
activeStep: 1
})
interface Step {
step: number;
title: string;
description: string;
icon: {
inactive: Component;
active: Component;
completed: Component;
};
};
const steps: readonly Step[] = [
{
step: 1,
title: "Create Device Password",
description: "Secure your device",
icon: {
inactive: LockClosedIcon,
active: LockClosedIcon,
completed: CheckIcon,
},
},
{
step: 2,
title: "Activate License",
description: "Create an Unraid.net account and activate your key",
icon: {
inactive: KeyIcon,
active: KeyIconSolid,
completed: CheckIcon,
},
},
{
step: 3,
title: "Unleash Your Hardware",
description: "Device is ready to configure",
icon: {
inactive: ServerStackIcon,
active: ServerStackIconSolid,
completed: CheckIcon,
},
},
] as const;
</script>
<template>
<Stepper
:default-value="activeStep"
class="text-foreground flex w-full items-start gap-2 text-16px"
>
<StepperItem
v-for="step in steps"
:key="step.step"
v-slot="{ state }: { state: StepState }"
class="relative flex w-full flex-col items-center justify-center"
:step="step.step"
:disabled="true"
>
<StepperSeparator
v-if="step.step !== steps[steps.length - 1].step"
class="absolute left-[calc(50%+20px)] right-[calc(-50%+10px)] top-[1.5rem] &block h-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"
/>
<StepperTrigger as-child>
<Button
:variant="state === 'completed' || state === 'active' ? 'default' : 'outline'"
size="default"
class="z-10 rounded-full shrink-0 opacity-100"
:class="[
state !== 'inactive' &&
'ring-2 ring-primary ring-offset-2 ring-offset-background *:cursor-default',
]"
:disabled="state === 'inactive'"
>
<component :is="step.icon[state]" class="size-4" />
</Button>
</StepperTrigger>
<div class="mt-2 flex flex-col items-center text-center">
<StepperTitle
:class="[state === 'active' && 'text-primary']"
class="text-2xs font-semibold transition"
>
{{ step.title }}
</StepperTitle>
<StepperDescription class="text-2xs font-normal">
{{ step.description }}
</StepperDescription>
</div>
</StepperItem>
</Stepper>
</template>

View File

@@ -58,12 +58,12 @@ const closeModal = () => {
const ariaLablledById = computed<string|undefined>(() => props.title ? `ModalTitle-${Math.random()}`.replace('0.', '') : undefined); const ariaLablledById = computed<string|undefined>(() => props.title ? `ModalTitle-${Math.random()}`.replace('0.', '') : undefined);
const computedVerticalCenter = computed<string>(() => { const computedVerticalCenter = computed<string>(() => {
if (props.tallContent) { if (props.tallContent) {
return 'items-start sm:items-center'; return 'justify-start sm:justify-center';
} }
if (typeof props.modalVerticalCenter === 'string') { if (typeof props.modalVerticalCenter === 'string') {
return props.modalVerticalCenter; return props.modalVerticalCenter;
} }
return props.modalVerticalCenter ? 'items-center' : 'items-start'; return props.modalVerticalCenter ? 'justify-center' : 'justify-start';
}); });
</script> </script>
@@ -78,7 +78,7 @@ const computedVerticalCenter = computed<string>(() => {
@keyup.esc="closeModal" @keyup.esc="closeModal"
> >
<div <div
class="fixed inset-0 flex min-h-screen w-screen justify-center p-8px sm:p-16px overflow-y-auto" class="fixed inset-0 flex flex-col min-h-screen w-screen items-center p-8px sm:p-16px overflow-y-auto"
:class="computedVerticalCenter" :class="computedVerticalCenter"
> >
<TransitionChild <TransitionChild
@@ -115,7 +115,7 @@ const computedVerticalCenter = computed<string>(() => {
success ? 'shadow-green-600/30 border-green-600/10' : '', success ? 'shadow-green-600/30 border-green-600/10' : '',
!error && !success && !disableShadow ? 'shadow-orange/10 border-white/10' : '', !error && !success && !disableShadow ? 'shadow-orange/10 border-white/10' : '',
]" ]"
class="text-16px text-foreground bg-background text-left relative z-10 flex flex-col justify-around border-2 border-solid transform overflow-hidden rounded-lg transition-all sm:w-full" class="text-16px text-foreground bg-background text-left relative z-10 mx-auto flex flex-col justify-around border-2 border-solid transform overflow-hidden rounded-lg transition-all sm:w-full"
> >
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 pt-4px pr-4px hidden sm:block"> <div v-if="showCloseX" class="absolute z-20 right-0 top-0 pt-4px pr-4px hidden sm:block">
<button <button
@@ -174,6 +174,21 @@ const computedVerticalCenter = computed<string>(() => {
</footer> </footer>
</div> </div>
</TransitionChild> </TransitionChild>
<TransitionChild
appear
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div v-if="$slots['subFooter']" class="mt-4 flex justify-center mx-auto" :class="[maxWidth]">
<slot name="subFooter" />
</div>
</TransitionChild>
</div> </div>
</div> </div>
</TransitionRoot> </TransitionRoot>

View File

@@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n';
import 'tailwindcss/tailwind.css'; import 'tailwindcss/tailwind.css';
import '~/assets/main.css'; import '~/assets/main.css';
import ActivationSteps from '~/components/Activation/Steps.vue';
import { useActivationCodeStore } from '~/store/activationCode'; import { useActivationCodeStore } from '~/store/activationCode';
import { useServerStore } from '~/store/server'; import { useServerStore } from '~/store/server';
import type { Server } from '~/types/server'; import type { Server } from '~/types/server';
@@ -28,7 +29,7 @@ const title = computed<string>(() =>
); );
const description = computed<string>(() => const description = computed<string>(() =>
t(`You're about to create a password to secure access to your system. This password is essential for managing and configuring your server. Youll use this password every time you access the Unraid web interface.`) t(`First, youll create your devices login credentials, then youll activate your Unraid license—your devices operating system (OS).`)
); );
const showModal = ref<boolean>(true); const showModal = ref<boolean>(true);
@@ -98,6 +99,10 @@ onBeforeMount(() => {
/> />
</div> </div>
</template> </template>
<template #subFooter>
<ActivationSteps :active-step="1" class="hidden sm:flex mt-6" />
</template>
</Modal> </Modal>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { StepperRootEmits, StepperRootProps } from 'radix-vue'
import { cn } from '@/components/shadcn/utils'
import { StepperRoot, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<StepperRootEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<StepperRoot
v-slot="slotProps"
:class="cn('flex gap-2', props.class)"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</StepperRoot>
</template>

View File

@@ -0,0 +1,27 @@
<script lang="ts" setup>
import type { StepperDescriptionProps } from 'radix-vue'
import { cn } from '@/components/shadcn/utils'
import { StepperDescription, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<StepperDescription
v-slot="slotProps"
v-bind="forwarded"
:class="cn('text-xs text-muted-foreground', props.class)"
>
<slot v-bind="slotProps" />
</StepperDescription>
</template>

View File

@@ -0,0 +1,37 @@
<script lang="ts" setup>
import type { StepperIndicatorProps } from 'radix-vue'
import { cn } from '@/components/shadcn/utils'
import { StepperIndicator, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperIndicatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<StepperIndicator
v-bind="forwarded"
:class="
cn(
'inline-flex items-center justify-center rounded-full text-muted-foreground/50 w-10 h-10',
// Disabled
'group-data-[disabled]:text-muted-foreground group-data-[disabled]:opacity-50',
// Active
'group-data-[state=active]:bg-primary group-data-[state=active]:text-primary-foreground',
// Completed
'group-data-[state=completed]:bg-accent group-data-[state=completed]:text-accent-foreground',
props.class
)
"
>
<slot />
</StepperIndicator>
</template>

View File

@@ -0,0 +1,29 @@
<script lang="ts" setup>
import type { StepperItemProps } from 'radix-vue'
import { cn } from '@/components/shadcn/utils'
import { StepperItem, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<StepperItem
v-slot="slotProps"
v-bind="forwarded"
:class="
cn('flex items-center gap-2 group data-[disabled]:pointer-events-none', props.class)
"
>
<slot v-bind="slotProps" />
</StepperItem>
</template>

View File

@@ -0,0 +1,33 @@
<script lang="ts" setup>
import type { StepperSeparatorProps } from 'radix-vue'
import { cn } from '@/components/shadcn/utils'
import { StepperSeparator, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<StepperSeparator
v-bind="forwarded"
:class="
cn(
'bg-muted',
// Disabled
'group-data-[disabled]:bg-muted group-data-[disabled]:opacity-75',
// Completed
'group-data-[state=completed]:bg-accent-foreground',
props.class
)
"
/>
</template>

View File

@@ -0,0 +1,26 @@
<script lang="ts" setup>
import type { StepperTitleProps } from 'radix-vue'
import { cn } from '@/components/shadcn/utils'
import { StepperTitle, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<StepperTitle
v-bind="forwarded"
:class="cn('text-md font-semibold whitespace-nowrap', props.class)"
>
<slot />
</StepperTitle>
</template>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { StepperTriggerProps } from 'radix-vue'
import { cn } from '@/components/shadcn/utils'
import { StepperTrigger, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<StepperTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<StepperTrigger
v-bind="forwarded"
:class="
cn('p-2 flex flex-col items-center text-center gap-2 rounded-md', props.class)
"
>
<slot />
</StepperTrigger>
</template>

View File

@@ -0,0 +1,7 @@
export { default as Stepper } from './Stepper.vue'
export { default as StepperDescription } from './StepperDescription.vue'
export { default as StepperIndicator } from './StepperIndicator.vue'
export { default as StepperItem } from './StepperItem.vue'
export { default as StepperSeparator } from './StepperSeparator.vue'
export { default as StepperTitle } from './StepperTitle.vue'
export { default as StepperTrigger } from './StepperTrigger.vue'

View File

@@ -35,6 +35,7 @@
"A valid OS version is required to check for OS updates.": "A valid OS version is required to check for OS updates.", "A valid OS version is required to check for OS updates.": "A valid OS version is required to check for OS updates.",
"Acklowledge that you have made a Flash Backup to enable this action": "Acklowledge that you have made a Flash Backup to enable this action", "Acklowledge that you have made a Flash Backup to enable this action": "Acklowledge that you have made a Flash Backup to enable this action",
"Activate Now": "Activate Now", "Activate Now": "Activate Now",
"Activate License": "Activate License",
"ago": "ago", "ago": "ago",
"All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.": "All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.", "All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.": "All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.",
"Attached Storage Devices": "Attached Storage Devices", "Attached Storage Devices": "Attached Storage Devices",
@@ -75,6 +76,7 @@
"Copy your Key URL: {0}": "Copy your Key URL: {0}", "Copy your Key URL: {0}": "Copy your Key URL: {0}",
"Create Flash Backup": "Create Flash Backup", "Create Flash Backup": "Create Flash Backup",
"Create a password": "Create a password", "Create a password": "Create a password",
"Create Device Password": "Create Device Password",
"Current Version {0}": "Current Version {0}", "Current Version {0}": "Current Version {0}",
"Current Version: Unraid {0}": "Current Version: Unraid {0}", "Current Version: Unraid {0}": "Current Version: Unraid {0}",
"Customizable Dashboard Tiles": "Customizable Dashboard Tiles", "Customizable Dashboard Tiles": "Customizable Dashboard Tiles",
@@ -175,7 +177,7 @@
"Learn more and link your key to your account": "Learn more and link your key to your account", "Learn more and link your key to your account": "Learn more and link your key to your account",
"Learn more": "Learn more", "Learn more": "Learn more",
"Learn More": "Learn More", "Learn More": "Learn More",
"Let's activate your Unraid license!": "Let's activate your Unraid license!", "Let's activate your Unraid OS License": "Let's activate your Unraid OS License",
"Let's Unleash your Hardware!": "Let's Unleash your Hardware!", "Let's Unleash your Hardware!": "Let's Unleash your Hardware!",
"License key actions": "License key actions", "License key actions": "License key actions",
"License key type": "License key type", "License key type": "License key type",
@@ -284,7 +286,7 @@
"SSL certificates for unraid.net deprecated": "SSL certificates for unraid.net deprecated", "SSL certificates for unraid.net deprecated": "SSL certificates for unraid.net deprecated",
"Stale Server": "Stale Server", "Stale Server": "Stale Server",
"Stale": "Stale", "Stale": "Stale",
"Start by creating an Unraid.net account — this will let you manage your license and access support. Once that's done, we'll guide you through a quick checkout process to register your license and install your key.": "Start by creating an Unraid.net account — this will let you manage your license and access support. Once that's done, we'll guide you through a quick checkout process to register your license and install your key.", "On the following screen, your license will be activated. You'll then create an Unraid.net Account to manage your license going forward.": "On the following screen, your license will be activated. You'll then create an Unraid.net Account to manage your license going forward.",
"Start Free 30 Day Trial": "Start Free 30 Day Trial", "Start Free 30 Day Trial": "Start Free 30 Day Trial",
"Starter": "Starter", "Starter": "Starter",
"Starting your free 30 day trial": "Starting your free 30 day trial", "Starting your free 30 day trial": "Starting your free 30 day trial",
@@ -317,6 +319,7 @@
"Unable to open release notes": "Unable to open release notes", "Unable to open release notes": "Unable to open release notes",
"Unknown error": "Unknown error", "Unknown error": "Unknown error",
"Unknown": "Unknown", "Unknown": "Unknown",
"Unleash Your Hardware": "Unleash Your Hardware",
"Unleashed": "Unleashed", "Unleashed": "Unleashed",
"unlimited": "unlimited", "unlimited": "unlimited",
"Unraid {0} Available": "Unraid {0} Available", "Unraid {0} Available": "Unraid {0} Available",
@@ -368,7 +371,7 @@
"You have exceeded the number of devices allowed for your license. Please remove a device before adding another.": "You have exceeded the number of devices allowed for your license. Please remove a device before adding another.", "You have exceeded the number of devices allowed for your license. Please remove a device before adding another.": "You have exceeded the number of devices allowed for your license. Please remove a device before adding another.",
"You have not activated the Flash Backup feature via the Unraid Connect plugin.": "You have not activated the Flash Backup feature via the Unraid Connect plugin.", "You have not activated the Flash Backup feature via the Unraid Connect plugin.": "You have not activated the Flash Backup feature via the Unraid Connect plugin.",
"You may still update to releases dated prior to your update expiration date.": "You may still update to releases dated prior to your update expiration date.", "You may still update to releases dated prior to your update expiration date.": "You may still update to releases dated prior to your update expiration date.",
"You're about to create a password to secure access to your system. This password is essential for managing and configuring your server. Youll use this password every time you access the Unraid web interface.": "You're about to create a password to secure access to your system. This password is essential for managing and configuring your server. Youll use this password every time you access the Unraid web interface.", "You're about to create a password to secure access to your system. This password is essential for managing and configuring your server. You'll use this password every time you access the Unraid web interface.": "You're about to create a password to secure access to your system. This password is essential for managing and configuring your server. You'll use this password every time you access the Unraid web interface.",
"You're one step closer to enhancing your Unraid experience": "You're one step closer to enhancing your Unraid experience", "You're one step closer to enhancing your Unraid experience": "You're one step closer to enhancing your Unraid experience",
"Your {0} Key has been replaced!": "Your {0} Key has been replaced!", "Your {0} Key has been replaced!": "Your {0} Key has been replaced!",
"Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates. You are still eligible to access OS updates that were published on or before {1}.": "Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates. You are still eligible to access OS updates that were published on or before {1}.", "Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates. You are still eligible to access OS updates that were published on or before {1}.": "Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates. You are still eligible to access OS updates that were published on or before {1}.",
@@ -377,5 +380,8 @@
"Your license key is not eligible for Unraid OS {0}": "Your license key is not eligible for Unraid OS {0}", "Your license key is not eligible for Unraid OS {0}": "Your license key is not eligible for Unraid OS {0}",
"Your license key's OS update eligibility has expired. Please renew your license key to enable updates released after your expiration date.": "Your license key's OS update eligibility has expired. Please renew your license key to enable updates released after your expiration date.", "Your license key's OS update eligibility has expired. Please renew your license key to enable updates released after your expiration date.": "Your license key's OS update eligibility has expired. Please renew your license key to enable updates released after your expiration date.",
"Your Trial has expired": "Your Trial has expired", "Your Trial has expired": "Your Trial has expired",
"Your Trial key has been extended!": "Your Trial key has been extended!" "Your Trial key has been extended!": "Your Trial key has been extended!",
"Create an Unraid.net account and activate your key": "Create an Unraid.net account and activate your key",
"Device is ready to configure": "Device is ready to configure",
"Secure your device": "Secure your device"
} }

View File

@@ -40,6 +40,57 @@ echo "$rsync_command"
eval "$rsync_command" eval "$rsync_command"
exit_code=$? exit_code=$?
# Update the auth-request.php file to include the new web component JS
update_auth_request() {
local server_name="$1"
# SSH into server and update auth-request.php
ssh "root@${server_name}" "
AUTH_REQUEST_FILE='/usr/local/emhttp/auth-request.php'
WEB_COMPS_DIR='/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/_nuxt/'
# Find JS files and modify paths
mapfile -t JS_FILES < <(find \"\$WEB_COMPS_DIR\" -type f -name \"*.js\" | sed 's|/usr/local/emhttp||' | sort -u)
FILES_TO_ADD+=(\"\${JS_FILES[@]}\")
if grep -q '\$arrWhitelist' \"\$AUTH_REQUEST_FILE\"; then
awk '
BEGIN { in_array = 0 }
/\\\$arrWhitelist\s*=\s*\[/ {
in_array = 1
print \$0
next
}
in_array && /^\s*\]/ {
in_array = 0
print \$0
next
}
!in_array || !/\/plugins\/dynamix\.my\.servers\/unraid-components\/_nuxt\/unraid-components\.client-/ {
print \$0
}
' \"\$AUTH_REQUEST_FILE\" > \"\${AUTH_REQUEST_FILE}.tmp\"
# Now add new entries right after the opening bracket
awk -v files_to_add=\"\$(printf '%s\n' \"\${FILES_TO_ADD[@]}\" | sort -u | awk '{printf \" \\\x27%s\\\x27,\n\", \$0}')\" '
/\\\$arrWhitelist\s*=\s*\[/ {
print \$0
print files_to_add
next
}
{ print }
' \"\${AUTH_REQUEST_FILE}.tmp\" > \"\${AUTH_REQUEST_FILE}\"
rm \"\${AUTH_REQUEST_FILE}.tmp\"
echo \"Updated \$AUTH_REQUEST_FILE with new web component JS files\"
else
echo \"\\\$arrWhitelist array not found in \$AUTH_REQUEST_FILE\"
fi
"
}
update_auth_request "$server_name"
# Play built-in sound based on the operating system # Play built-in sound based on the operating system
if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS # macOS