mirror of
https://github.com/unraid/api.git
synced 2026-01-03 15:09:48 -06:00
feat: WIP error store progress with server data
This commit is contained in:
@@ -37,7 +37,7 @@ if (state === 'TRIAL') expireTime = Date.now() + 60 * 60 * 1000; // in 1 hour
|
||||
if (state === 'EEXPIRED') expireTime = uptime; // 1 hour ago
|
||||
|
||||
const serverState = {
|
||||
"apiKey": "unupc_12312313123",
|
||||
"apiKey": "XXXunupc_12312313123",
|
||||
"avatar": "https://source.unsplash.com/300x300/?portrait",
|
||||
"description": "DevServer9000",
|
||||
"deviceCount": "3",
|
||||
@@ -51,9 +51,8 @@ const serverState = {
|
||||
"license": "",
|
||||
"locale": "en_US",
|
||||
"name": "fuji",
|
||||
// "pluginInstalled": "dynamix.unraid.net.staging.plg",
|
||||
"pluginInstalled": true,
|
||||
"registered": true,
|
||||
"pluginInstalled": "dynamix.unraid.net.staging.plg",
|
||||
"registered": false,
|
||||
"regGen": 0,
|
||||
// "regGuid": "0781-5583-8355-81071A2B0211",
|
||||
"site": "http://localhost:4321",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
||||
export interface Props {
|
||||
export interface ButtonProps {
|
||||
btnStyle?: 'fill' | 'outline' | 'underline';
|
||||
btnType?: 'button' | 'submit' | 'reset';
|
||||
download?: boolean;
|
||||
@@ -9,7 +9,7 @@ export interface Props {
|
||||
icon?: typeof XCircleIcon;
|
||||
text?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
btnStyle: 'fill',
|
||||
btnType: 'button',
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ const { text, copy, copied, isSupported } = useClipboard({ source: keyUrl.value
|
||||
<template #main>
|
||||
<div
|
||||
v-if="keyInstallStatus !== 'ready' || accountActionStatus !== 'ready'"
|
||||
class="text-center relative w-full min-h-[15vh] flex flex-col justify-center gap-y-16px py-16px"
|
||||
class="text-center relative w-full flex flex-col justify-center gap-y-16px py-24px sm:py-32px"
|
||||
>
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="keyInstallStatus !== 'ready'"
|
||||
|
||||
@@ -81,8 +81,8 @@ const links = computed(():UserProfileLink[] => {
|
||||
</span>
|
||||
</header>
|
||||
<ul class="list-reset flex flex-col gap-y-4px p-0">
|
||||
<UpcDropdownError v-if="stateData.error" />
|
||||
<UpcDropdownConnectStatus v-else-if="!stateData.error && registered && pluginInstalled" class="mt-8px" />
|
||||
<UpcDropdownError />
|
||||
<UpcDropdownConnectStatus v-if="!stateData.error && registered && pluginInstalled" class="mt-8px" />
|
||||
|
||||
<li class="m-8px">
|
||||
<UpcKeyline />
|
||||
|
||||
@@ -1,37 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { InformationCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { CONNECT_FORUMS } from '~/helpers/urls';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
// import {
|
||||
// ExclamationCircleIcon,
|
||||
// ExclamationTriangleIcon,
|
||||
// ShieldExclamationIcon,
|
||||
// } from '@heroicons/vue/24/solid';
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
import { useErrorsStore, type Error } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
const errorsStore = useErrorsStore();
|
||||
const { errors } = storeToRefs(errorsStore);
|
||||
const { stateData } = storeToRefs(useServerStore());
|
||||
const links = ref<UserProfileLink[]>([
|
||||
// {
|
||||
// click: () => accountStore.troubleshoot(),
|
||||
// external: true,
|
||||
// icon: InformationCircleIcon,
|
||||
// text: 'Placeholder Button',
|
||||
// },
|
||||
// {
|
||||
// external: true,
|
||||
// href: CONNECT_FORUMS,
|
||||
// icon: InformationCircleIcon,
|
||||
// text: 'Connect Support Forum',
|
||||
// },
|
||||
]);
|
||||
|
||||
const computedErrors = computed(() => {
|
||||
if (stateData.value?.error) {
|
||||
return [
|
||||
{
|
||||
heading: stateData.value.heading,
|
||||
level: 'error',
|
||||
message: stateData.value.message,
|
||||
},
|
||||
];
|
||||
};
|
||||
return errors.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul v-if="stateData.error" class="text-white bg-unraid-red/90 font-semibold list-reset flex flex-col gap-y-4px mb-4px py-12px px-16px rounded">
|
||||
<h3 class="text-18px">{{ stateData.heading }}</h3>
|
||||
<p class="text-14px">{{ stateData.message }}</p>
|
||||
<template v-if="links">
|
||||
<li v-for="(link, index) in links" :key="`link_${index}`" class="-mx-8px">
|
||||
<UpcDropdownItem :item="link" class="text-white" />
|
||||
</li>
|
||||
</template>
|
||||
<ul v-if="computedErrors" class="text-white bg-unraid-red/90 font-semibold list-reset flex flex-col gap-y-8px mb-4px py-12px px-16px rounded">
|
||||
<li v-for="(error, index) in computedErrors" :key="index" class="flex flex-col gap-8px">
|
||||
<h3 class="text-18px">
|
||||
<span>{{ error.heading }}</span>
|
||||
</h3>
|
||||
<div v-html="error.message" class="text-14px"></div>
|
||||
<nav v-if="error.actions">
|
||||
<li v-for="(link, index) in error.actions" :key="`link_${index}`" class="-mx-8px">
|
||||
<UpcDropdownItem :item="link" class="text-white" />
|
||||
</li>
|
||||
</nav>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { InformationCircleIcon, ExclamationTriangleIcon } from '@heroicons/vue/24/solid';
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
const dropdownStore = useDropdownStore();
|
||||
const { dropdownVisible } = storeToRefs(dropdownStore);
|
||||
const { errors } = storeToRefs(useErrorsStore());
|
||||
const {
|
||||
pluginInstalled,
|
||||
pluginOutdated,
|
||||
@@ -16,7 +18,7 @@ const {
|
||||
} = storeToRefs(useServerStore());
|
||||
|
||||
const registeredAndPluginInstalled = computed(() => pluginInstalled.value && registered.value);
|
||||
const showErrorIcon = computed(() => stateData.value.error);
|
||||
const showErrorIcon = computed(() => errors.value.length || stateData.value.error);
|
||||
|
||||
const text = computed((): string | undefined => {
|
||||
if ((stateData.value.error) && state.value !== 'EEXPIRED') return 'Fix Error';
|
||||
@@ -27,11 +29,7 @@ const text = computed((): string | undefined => {
|
||||
const title = computed((): string => {
|
||||
if (state.value === 'ENOKEYFILE') return 'Get Started';
|
||||
if (state.value === 'EEXPIRED') return 'Trial Expired, see options below';
|
||||
if (stateData.value.error) return 'Learn More';
|
||||
// if (cloud.value && cloud.value.error) return 'Unraid API Error';
|
||||
// if (myServersError.value && registeredAndPluginInstalled.value return 'Unraid API Error';
|
||||
// if (errorTooManyDisks.value) return 'Too many devices';
|
||||
// if (isLaunchpadOpen.value) return 'Close and continue to webGUI';
|
||||
if (showErrorIcon.value) return 'Learn more about the error';
|
||||
return dropdownVisible.value ? 'Close Dropdown' : 'Open Dropdown';
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -6,8 +6,10 @@ const CONNECT_DOCS = 'https://docs.unraid.net/category/unraid-connect';
|
||||
const CONNECT_DASHBOARD = 'https://connect.myunraid.net';
|
||||
const CONNECT_FORUMS = 'https://forums.unraid.net/forum/94-connect-plugin-support/';
|
||||
const DEV_GRAPH_URL = '';
|
||||
const PURCHASE = 'https://unraid.net/preflight';
|
||||
const PLUGIN_SETTINGS = '/Settings/ManagementAccess#UnraidNetSettings';
|
||||
const PURCHASE = 'https://unraid.ddev.site/callback';
|
||||
|
||||
const SETTINGS_MANAGMENT_ACCESS = '/Settings/ManagementAccess';
|
||||
const PLUGIN_SETTINGS = `${SETTINGS_MANAGMENT_ACCESS}#UnraidNetSettings`;
|
||||
|
||||
export {
|
||||
ACCOUNT,
|
||||
@@ -17,4 +19,5 @@ export {
|
||||
DEV_GRAPH_URL,
|
||||
PURCHASE,
|
||||
PLUGIN_SETTINGS,
|
||||
SETTINGS_MANAGMENT_ACCESS,
|
||||
};
|
||||
@@ -1,8 +1,11 @@
|
||||
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
// import { useAccountStore } from '~/store/account';
|
||||
// import { useCallbackStore, useCallbackActionsStore } from '~/store/callbackActions';
|
||||
// import { useInstallKeyStore } from '~/store/installKey';
|
||||
// import { useServerStore } from '~/store/server';
|
||||
import type { ButtonProps } from '~/components/Brand/Button.vue';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
@@ -10,6 +13,17 @@ import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export type ErrorType = 'account' | 'callback' | 'installKey' | 'server' | 'serverState';
|
||||
export interface Error {
|
||||
actions?: ButtonProps[];
|
||||
heading: string;
|
||||
level: 'error' | 'info' | 'warning';
|
||||
message: string;
|
||||
ref?: string;
|
||||
supportLink?: boolean;
|
||||
type: ErrorType;
|
||||
}
|
||||
|
||||
export const useErrorsStore = defineStore('errors', () => {
|
||||
// const accountStore = useAccountStore();
|
||||
// const callbackStore = useCallbackStore();
|
||||
@@ -17,13 +31,16 @@ export const useErrorsStore = defineStore('errors', () => {
|
||||
// const installKeyStore = useInstallKeyStore();
|
||||
// const serverStore = useServerStore();
|
||||
|
||||
/** @todo type the errors */
|
||||
const errors = ref<any[]>([]);
|
||||
const errors = ref<Error[]>([]);
|
||||
|
||||
const removeError = (index: number) => {
|
||||
const removeErrorByIndex = (index: number) => {
|
||||
errors.value = errors.value.filter((_error, i) => i !== index);
|
||||
};
|
||||
|
||||
const removeErrorByRef = (ref: ErrorType) => {
|
||||
errors.value = errors.value.filter(error => error?.ref !== ref);
|
||||
};
|
||||
|
||||
const resetErrors = () => {
|
||||
errors.value = [];
|
||||
};
|
||||
@@ -34,7 +51,8 @@ export const useErrorsStore = defineStore('errors', () => {
|
||||
|
||||
return {
|
||||
errors,
|
||||
removeError,
|
||||
removeErrorByIndex,
|
||||
removeErrorByRef,
|
||||
resetErrors,
|
||||
setError,
|
||||
};
|
||||
|
||||
134
store/server.ts
134
store/server.ts
@@ -1,18 +1,27 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { ArrowRightOnRectangleIcon, GlobeAltIcon, KeyIcon } from '@heroicons/vue/24/solid';
|
||||
import {
|
||||
ArrowRightOnRectangleIcon,
|
||||
CogIcon,
|
||||
GlobeAltIcon,
|
||||
KeyIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
|
||||
import { useAccountStore } from './account';
|
||||
import { usePurchaseStore } from "./purchase";
|
||||
import { useTrialStore } from './trial';
|
||||
import { useThemeStore, type Theme } from './theme';
|
||||
import { SETTINGS_MANAGMENT_ACCESS } from '~/helpers/urls';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useErrorsStore, type Error } from '~/store/errors';
|
||||
import { usePurchaseStore } from "~/store/purchase";
|
||||
import { useTrialStore } from '~/store/trial';
|
||||
import { useThemeStore, type Theme } from '~/store/theme';
|
||||
import type {
|
||||
Server,
|
||||
ServerAccountCallbackSendPayload,
|
||||
ServerKeyTypeForPurchase,
|
||||
ServerPurchaseCallbackSendPayload,
|
||||
ServerState,
|
||||
ServerStateConfigStatus,
|
||||
ServerStateData,
|
||||
ServerStateDataAction,
|
||||
ServerPluginInstalled,
|
||||
} from '~/types/server';
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
@@ -22,6 +31,7 @@ setActivePinia(createPinia());
|
||||
|
||||
export const useServerStore = defineStore('server', () => {
|
||||
const accountStore = useAccountStore();
|
||||
const errorsStore = useErrorsStore();
|
||||
const purchaseStore = usePurchaseStore();
|
||||
const themeStore = useThemeStore();
|
||||
const trialStore = useTrialStore();
|
||||
@@ -30,6 +40,7 @@ export const useServerStore = defineStore('server', () => {
|
||||
*/
|
||||
const avatar = ref<string>(''); // @todo potentially move to a user store
|
||||
const apiKey = ref<string>(''); // @todo potentially move to a user store
|
||||
const config = ref<ServerStateConfigStatus>();
|
||||
const csrf = ref<string>(''); // required to make requests to Unraid webgui
|
||||
const description = ref<string>('');
|
||||
const deviceCount = ref<number>(0);
|
||||
@@ -47,7 +58,7 @@ export const useServerStore = defineStore('server', () => {
|
||||
const license = ref<string>('');
|
||||
const locale = ref<string>('');
|
||||
const name = ref<string>('');
|
||||
const pluginInstalled = ref<boolean>(false);
|
||||
const pluginInstalled = ref<ServerPluginInstalled>('');
|
||||
const registered = ref<boolean>();
|
||||
const regGen = ref<number>(0);
|
||||
const regGuid = ref<string>('');
|
||||
@@ -57,7 +68,6 @@ export const useServerStore = defineStore('server', () => {
|
||||
const uptime = ref<number>(0);
|
||||
const username = ref<string>(''); // @todo potentially move to a user store
|
||||
const wanFQDN = ref<string>('');
|
||||
|
||||
/**
|
||||
* Getters
|
||||
*/
|
||||
@@ -428,6 +438,81 @@ export const useServerStore = defineStore('server', () => {
|
||||
});
|
||||
const trialExtensionEligible = computed(() => !regGen.value || regGen.value < 2);
|
||||
|
||||
const invalidApiKey = computed((): Error | undefined => {
|
||||
// must be registered with plugin installed
|
||||
if (!registered.value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Keeping separate from validApiKeyLength because we may want to add more checks. Cloud also help with debugging user error submissions.
|
||||
if (apiKey.value.length !== 64) {
|
||||
console.debug('[invalidApiKey] invalid length');
|
||||
return {
|
||||
heading: 'Invalid API Key',
|
||||
level: 'error',
|
||||
message: 'Please sign out then sign back in to refresh your API key.',
|
||||
ref: 'invalidApiKeyLength',
|
||||
type: 'server',
|
||||
};
|
||||
}
|
||||
if (!apiKey.value.startsWith('unupc_')) {
|
||||
console.debug('[invalidApiKey] invalid for upc');
|
||||
return {
|
||||
heading: 'Invalid API Key Format',
|
||||
level: 'error',
|
||||
message: 'Please sign out then sign back in to refresh your API key.',
|
||||
ref: 'invalidApiKeyFormat',
|
||||
type: 'server',
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const tooManyDevices = computed((): Error | undefined => {
|
||||
if (!config.value?.valid && config.value?.error === 'INVALID') {
|
||||
return {
|
||||
heading: 'Too Many Devices',
|
||||
level: 'error',
|
||||
message: 'You have exceeded the number of devices allowed for your license. Please remove a device before adding another.',
|
||||
ref: 'tooManyDevices',
|
||||
type: 'server',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const pluginInstallFailed = computed((): Error | undefined => {
|
||||
if (pluginInstalled.value && pluginInstalled.value.includes('_installFailed')) {
|
||||
return {
|
||||
heading: 'Unraid Connect Install Failed',
|
||||
level: 'error',
|
||||
message: 'Rebooting will likely solve this.',
|
||||
ref: 'pluginInstallFailed',
|
||||
type: 'server',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Deprecation warning for [hash].unraid.net SSL certs. Deprecation started 2023-01-01
|
||||
*/
|
||||
const deprecatedUnraidSSL = computed((): Error | undefined => {
|
||||
if (window.location.hostname.includes('.unraid.net')) {
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
href: SETTINGS_MANAGMENT_ACCESS,
|
||||
icon: CogIcon,
|
||||
text: 'Go to Management Access Now',
|
||||
},
|
||||
],
|
||||
heading: 'Unraid.net SSL Certificate Deprecation',
|
||||
level: 'warning',
|
||||
message: 'Unraid.net SSL certificates will be deprecated on January 1st, 2023. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.',
|
||||
ref: 'deprecatedUnraidSSL',
|
||||
type: 'server',
|
||||
};
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
@@ -461,14 +546,45 @@ export const useServerStore = defineStore('server', () => {
|
||||
console.debug('[setServer] server.value', server.value);
|
||||
};
|
||||
|
||||
watch(theme, () => {
|
||||
if (theme.value) themeStore.setTheme(theme.value);
|
||||
watch(theme, (newVal) => {
|
||||
if (newVal) themeStore.setTheme(newVal);
|
||||
});
|
||||
|
||||
watch(stateData, (newVal, oldVal) => {
|
||||
if (oldVal.error) {
|
||||
errorsStore.removeErrorByRef('serverState');
|
||||
}
|
||||
if (newVal.error && !oldVal.error) {
|
||||
const stateDataError = {
|
||||
heading: newVal.heading,
|
||||
message: newVal.message,
|
||||
type: 'serverState',
|
||||
};
|
||||
errorsStore.setError(stateDataError);
|
||||
}
|
||||
});
|
||||
watch(invalidApiKey, (newVal, oldVal) => {
|
||||
if (oldVal && oldVal.ref) errorsStore.removeErrorByRef(oldVal.ref);
|
||||
if (newVal) errorsStore.setError(newVal);
|
||||
});
|
||||
watch(tooManyDevices, (newVal, oldVal) => {
|
||||
if (oldVal && oldVal.ref) errorsStore.removeErrorByRef(oldVal.ref);
|
||||
if (newVal) errorsStore.setError(newVal);
|
||||
});
|
||||
watch(pluginInstallFailed, (newVal, oldVal) => {
|
||||
if (oldVal && oldVal.ref) errorsStore.removeErrorByRef(oldVal.ref);
|
||||
if (newVal) errorsStore.setError(newVal);
|
||||
});
|
||||
watch(deprecatedUnraidSSL, (newVal, oldVal) => {
|
||||
if (oldVal && oldVal.ref) errorsStore.removeErrorByRef(oldVal.ref);
|
||||
if (newVal) errorsStore.setError(newVal);
|
||||
});
|
||||
|
||||
return {
|
||||
// state
|
||||
apiKey,
|
||||
avatar,
|
||||
config,
|
||||
csrf,
|
||||
description,
|
||||
deviceCount,
|
||||
|
||||
@@ -2,6 +2,10 @@ import { KeyIcon } from '@heroicons/vue/24/solid';
|
||||
import { Theme } from '~/store/theme';
|
||||
import { UserProfileLink } from '~/types/userProfile';
|
||||
|
||||
export interface ServerStateConfigStatus {
|
||||
error: 'INVALID' | 'NO_KEY_SERVER' | 'UNKNOWN_ERROR' | 'WITHDRAWN';
|
||||
valid: boolean;
|
||||
}
|
||||
export type ServerState = 'BASIC'
|
||||
| 'PLUS'
|
||||
| 'PRO'
|
||||
@@ -29,6 +33,7 @@ export type ServerState = 'BASIC'
|
||||
export interface Server {
|
||||
apiKey?: string;
|
||||
avatar?: string;
|
||||
config?: ServerStateConfigStatus | undefined;
|
||||
csrf?: string;
|
||||
description?: string;
|
||||
deviceCount?: number;
|
||||
@@ -43,7 +48,7 @@ export interface Server {
|
||||
license?: string;
|
||||
locale?: string;
|
||||
name?: string;
|
||||
pluginInstalled?: boolean;
|
||||
pluginInstalled?: ServerPluginInstalled;
|
||||
registered?: boolean;
|
||||
regGen?: number;
|
||||
regGuid?: string;
|
||||
@@ -113,4 +118,6 @@ export interface ServerStateData {
|
||||
message?: string;
|
||||
error?: ServerStateDataError | boolean;
|
||||
withKey?: boolean; // @todo potentially remove
|
||||
}
|
||||
}
|
||||
|
||||
export type ServerPluginInstalled = 'dynamix.unraid.net.plg' | 'dynamix.unraid.net.staging.plg' | 'dynamix.unraid.net.plg_installFailed' | 'dynamix.unraid.net.staging.plg_installFailed' | '';
|
||||
|
||||
Reference in New Issue
Block a user