feat(sh-admin): introducing infra-tokens to admin dashboard (#4202)

Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
Joel Jacob Stephen
2024-07-29 15:20:06 +03:00
committed by GitHub
parent 783d911f8d
commit c24d5c5302
18 changed files with 753 additions and 74 deletions

View File

@@ -1,63 +1,67 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
import '@vue/runtime-core';
export {}
export {};
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AppHeader: typeof import('./components/app/Header.vue')['default']
AppLogin: typeof import('./components/app/Login.vue')['default']
AppLogout: typeof import('./components/app/Logout.vue')['default']
AppModal: typeof import('./components/app/Modal.vue')['default']
AppSidebar: typeof import('./components/app/Sidebar.vue')['default']
AppToast: typeof import('./components/app/Toast.vue')['default']
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default']
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable']
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideCheck: typeof import('~icons/lucide/check')['default']
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUser: typeof import('~icons/lucide/user')['default']
SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default']
SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default']
SettingsDataSharing: typeof import('./components/settings/DataSharing.vue')['default']
SettingsReset: typeof import('./components/settings/Reset.vue')['default']
SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default']
SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.vue')['default']
SetupDataSharingAndNewsletter: typeof import('./components/setup/DataSharingAndNewsletter.vue')['default']
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
TeamsDetails: typeof import('./components/teams/Details.vue')['default']
TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
TeamsMembers: typeof import('./components/teams/Members.vue')['default']
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default']
Tippy: typeof import('vue-tippy')['Tippy']
UiAutoResetIcon: typeof import('./components/ui/AutoResetIcon.vue')['default']
UsersDetails: typeof import('./components/users/Details.vue')['default']
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default']
UsersSuccessInviteModal: typeof import('./components/users/SuccessInviteModal.vue')['default']
AppHeader: typeof import('./components/app/Header.vue')['default'];
AppLogin: typeof import('./components/app/Login.vue')['default'];
AppLogout: typeof import('./components/app/Logout.vue')['default'];
AppModal: typeof import('./components/app/Modal.vue')['default'];
AppSidebar: typeof import('./components/app/Sidebar.vue')['default'];
AppToast: typeof import('./components/app/Toast.vue')['default'];
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default'];
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'];
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'];
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'];
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete'];
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox'];
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'];
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'];
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection'];
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'];
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink'];
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'];
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'];
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder'];
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper'];
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'];
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab'];
HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable'];
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs'];
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle'];
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default'];
IconLucideCheck: typeof import('~icons/lucide/check')['default'];
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default'];
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default'];
IconLucideInbox: typeof import('~icons/lucide/inbox')['default'];
IconLucideSearch: typeof import('~icons/lucide/search')['default'];
IconLucideUser: typeof import('~icons/lucide/user')['default'];
SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default'];
SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default'];
SettingsDataSharing: typeof import('./components/settings/DataSharing.vue')['default'];
SettingsReset: typeof import('./components/settings/Reset.vue')['default'];
SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default'];
SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.vue')['default'];
SetupDataSharingAndNewsletter: typeof import('./components/setup/DataSharingAndNewsletter.vue')['default'];
TeamsAdd: typeof import('./components/teams/Add.vue')['default'];
TeamsDetails: typeof import('./components/teams/Details.vue')['default'];
TeamsInvite: typeof import('./components/teams/Invite.vue')['default'];
TeamsMembers: typeof import('./components/teams/Members.vue')['default'];
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default'];
Tippy: typeof import('vue-tippy')['Tippy'];
Tokens: typeof import('./components/tokens/index.vue')['default'];
TokensGenerateModal: typeof import('./components/tokens/GenerateModal.vue')['default'];
TokensList: typeof import('./components/tokens/List.vue')['default'];
TokensOverview: typeof import('./components/tokens/Overview.vue')['default'];
TokensToken: typeof import('./components/tokens/Token.vue')['default'];
UiAutoResetIcon: typeof import('./components/ui/AutoResetIcon.vue')['default'];
UsersDetails: typeof import('./components/users/Details.vue')['default'];
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'];
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default'];
UsersSuccessInviteModal: typeof import('./components/users/SuccessInviteModal.vue')['default'];
}
}

View File

@@ -1,16 +1,13 @@
<template>
<div
v-if="workingConfigs"
class="md:grid md:grid-cols-3 md:gap-4 border-divider border-b"
>
<div class="pb-8 px-8 md:col-span-1">
<div v-if="workingConfigs" class="grid md:grid-cols-3 gap-8 md:gap-4">
<div class="md:col-span-1">
<h3 class="heading">{{ t('configs.auth_providers.title') }}</h3>
<p class="my-1 text-secondaryLight">
{{ t('configs.auth_providers.description') }}
</p>
</div>
<div class="space-y-8 p-8 md:col-span-2">
<div class="space-y-8 sm:px-8 md:col-span-2">
<section>
<h4 class="font-semibold text-secondaryDark">
{{ t('configs.auth_providers.title') }}

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="flex flex-col space-y-8 divide-y divide-divider">
<SettingsAuthProvider v-model:config="workingConfigs" />
<SettingsSmtpConfiguration v-model:config="workingConfigs" />
<SettingsDataSharing v-model:config="workingConfigs" />

View File

@@ -1,13 +1,13 @@
<template>
<div class="md:grid md:grid-cols-3 md:gap-4 border-divider border-b py-8">
<div class="px-8 md:col-span-1">
<div class="grid md:grid-cols-3 gap-8 md:gap-4 pt-8">
<div class="md:col-span-1">
<h3 class="heading">{{ t('configs.data_sharing.title') }}</h3>
<p class="my-1 text-secondaryLight">
{{ t('configs.data_sharing.description') }}
</p>
</div>
<div class="mx-8 md:col-span-2">
<div class="sm:px-8 md:col-span-2">
<h4 class="font-semibold text-secondaryDark">
{{ t('configs.data_sharing.title') }}
</h4>

View File

@@ -1,12 +1,12 @@
<template>
<div class="md:grid md:grid-cols-3 md:gap-4">
<div class="p-8 md:col-span-1">
<div class="grid md:grid-cols-3 gap-8 md:gap-4 pt-8">
<div class="md:col-span-1">
<h3 class="heading">{{ t('configs.reset.title') }}</h3>
<p class="my-1 text-secondaryLight">
{{ t('configs.reset.description') }}
</p>
</div>
<div class="space-y-8 p-8 md:col-span-2">
<div class="space-y-8 sm:px-8 md:col-span-2">
<section>
<h4 class="font-semibold text-secondaryDark">
{{ t('configs.reset.info') }}

View File

@@ -1,16 +1,13 @@
<template>
<div
v-if="smtpConfigs"
class="md:grid md:grid-cols-3 md:gap-4 border-divider border-b"
>
<div class="p-8 px-8 md:col-span-1">
<div v-if="smtpConfigs" class="grid md:grid-cols-3 gap-4 md:gap-4 pt-8">
<div class="md:col-span-1">
<h3 class="heading">{{ t('configs.mail_configs.title') }}</h3>
<p class="my-1 text-secondaryLight">
{{ t('configs.mail_configs.description') }}
</p>
</div>
<div class="space-y-8 p-8 md:col-span-2">
<div class="space-y-8 sm:px-8 md:col-span-2">
<section>
<h4 class="font-semibold text-secondaryDark">
{{ t('configs.mail_configs.title') }}

View File

@@ -0,0 +1,210 @@
<template>
<HoppSmartModal
dialog
:title="t('infra_tokens.generate_modal_title')"
@close="hideModal"
>
<template #body>
<template v-if="infraToken">
<p class="p-4 mb-4 border rounded-md text-amber-500 border-amber-600">
{{ t('infra_tokens.copy_token_warning') }}
</p>
<div
class="flex items-center justify-between p-4 mt-4 rounded-md bg-primaryLight"
>
<div class="text-secondaryDark">{{ infraToken }}</div>
<HoppButtonSecondary
outline
filled
:icon="copyIcon"
@click="copyInfraToken"
/>
</div>
</template>
<div v-else class="space-y-4">
<div class="space-y-2">
<div class="font-semibold text-secondaryDark">
{{ t('action.label') }}
</div>
<HoppSmartInput
v-model="infraTokenLabel"
:placeholder="t('infra_tokens.token_purpose')"
/>
</div>
<div class="space-y-2">
<label for="expiration" class="font-semibold text-secondaryDark">{{
t('infra_tokens.expiration_label')
}}</label>
<div class="grid items-center grid-cols-2 gap-x-2">
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions?.focus()"
>
<HoppSmartSelectWrapper>
<input
id="expiration"
:value="expiration"
readonly
class="flex flex-1 px-4 py-2 bg-transparent border rounded cursor-pointer border-divider"
/>
</HoppSmartSelectWrapper>
<template #content="{ hide }">
<div
ref="tippyActions"
tabindex="0"
role="menu"
class="flex flex-col focus:outline-none"
@keyup.escape="hide"
>
<HoppSmartItem
v-for="expirationOption in Object.keys(expirationOptions)"
:key="expirationOption"
:label="expirationOption"
:icon="
expirationOption === expiration
? IconCircleDot
: IconCircle
"
:active="expirationOption === expiration"
:aria-selected="expirationOption === expiration"
@click="
() => {
expiration = expirationOption;
hide();
}
"
/>
</div>
</template>
</tippy>
<span class="text-secondaryLight">{{ expirationDateText }}</span>
</div>
</div>
</div>
</template>
<template #footer>
<HoppButtonSecondary
v-if="infraToken"
:label="t('action.close')"
outline
filled
@click="hideModal"
/>
<div v-else class="flex items-center gap-x-2">
<HoppButtonPrimary
:loading="tokenGenerateActionLoading"
filled
outline
:label="t('infra_tokens.generate_token')"
@click="generateInfraToken"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
outline
filled
@click="hideModal"
/>
</div>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { refAutoReset } from '@vueuse/core';
import { VNodeRef, computed, ref } from 'vue';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { copyToClipboard } from '~/helpers/utils/clipboard';
import { shortDateTime } from '~/helpers/utils/date';
import IconCheck from '~icons/lucide/check';
import IconCircle from '~icons/lucide/circle';
import IconCircleDot from '~icons/lucide/circle-dot';
import IconCopy from '~icons/lucide/copy';
const t = useI18n();
const toast = useToast();
const props = defineProps<{
tokenGenerateActionLoading: boolean;
infraToken: string | null;
}>();
const emit = defineEmits<{
(e: 'hide-modal'): void;
(
e: 'generate-infra-token',
{ label, expiryInDays }: { label: string; expiryInDays: number | null }
): void;
}>();
// Template refs
const tippyActions = ref<VNodeRef | null>(null);
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
);
const infraTokenLabel = ref<string>('');
const expiration = ref<string>('30 days');
const expirationOptions: Record<string, number | null> = {
'7 days': 7,
'30 days': 30,
'60 days': 60,
'90 days': 90,
'No expiration': null,
};
const expirationDateText = computed(() => {
const chosenExpiryInDays = expirationOptions[expiration.value];
if (chosenExpiryInDays === null) {
return t('infra_tokens.no_expiration_verbose');
}
const currentDate = new Date();
currentDate.setDate(currentDate.getDate() + chosenExpiryInDays);
const expirationDate = shortDateTime(currentDate, false);
return `${t('infra_tokens.token_expires_on')} ${expirationDate}`;
});
const copyInfraToken = () => {
if (!props.infraToken) {
toast.error('state.something_went_wrong');
return;
}
copyToClipboard(props.infraToken);
copyIcon.value = IconCheck;
toast.success(t('state.copied_to_clipboard'));
};
const generateInfraToken = async () => {
if (!infraTokenLabel.value) {
toast.error(t('infra_tokens.invalid_label'));
return;
}
emit('generate-infra-token', {
label: infraTokenLabel.value,
expiryInDays: expirationOptions[expiration.value],
});
};
const hideModal = () => emit('hide-modal');
</script>

View File

@@ -0,0 +1,130 @@
<template>
<div class="flex flex-col px-4">
<div v-if="isInitialPageLoad" class="flex flex-col items-center py-3">
<HoppSmartSpinner />
</div>
<div
v-else-if="initialPageLoadHasError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ t('state.something_went_wrong') }}
</div>
<HoppSmartPlaceholder
v-else-if="infraTokens.length === 0"
:src="noTokensImage"
:alt="`${t('infra_tokens.empty')}`"
:text="t('infra_tokens.empty')"
@drop.stop
/>
<div v-else class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<div
v-for="{ id, label, lastUsedOn, expiresOn } in infraTokens"
:key="id"
class="flex flex-col items-center gap-4 p-4 border rounded border-divider"
>
<div class="w-full text-sm font-semibold truncate text-secondaryDark">
{{ label }}
</div>
<div class="flex items-center justify-between w-full gap-x-4">
<div class="space-y-1 text-secondaryLight">
<div class="space-x-1">
<span class="font-semibold"
>{{ t('infra_tokens.last_used_on') }}:</span
>
<span>
{{ shortDateTime(lastUsedOn, false) }}
</span>
</div>
<div class="space-x-1">
<span class="font-semibold"
>{{ t('infra_tokens.expires_on') }}:</span
>
<span>
{{ getTokenExpiryText(expiresOn) }}
</span>
</div>
</div>
<HoppButtonSecondary
:label="t('action.delete')"
filled
outline
@click="
emit('delete-infra-token', {
tokenId: id,
tokenLabel: label,
})
"
/>
</div>
</div>
</div>
<HoppSmartIntersection
v-if="hasMoreTokens"
@intersecting="emit('fetch-more-tokens')"
>
<div v-if="loading" class="flex flex-col items-center py-3">
<HoppSmartSpinner />
</div>
<div v-else-if="hasError" class="flex flex-col items-center py-4">
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ t('state.something_went_wrong') }}
</div>
</HoppSmartIntersection>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from '~/composables/i18n';
import { InfraTokensQuery } from '~/helpers/backend/graphql';
import { shortDateTime } from '~/helpers/utils/date';
const t = useI18n();
const props = defineProps<{
infraTokens: InfraTokensQuery['infraTokens'];
hasMoreTokens: boolean;
loading: boolean;
hasError: boolean;
}>();
const emit = defineEmits<{
(e: 'fetch-more-tokens'): void;
(
e: 'delete-infra-token',
{ tokenId, tokenLabel }: { tokenId: string; tokenLabel: string }
): void;
}>();
const noTokensImage = `${
import.meta.env.VITE_ADMIN_URL
}/assets/images/pack.svg`;
const isInitialPageLoad = computed(() => props.loading && !props.hasMoreTokens);
const initialPageLoadHasError = computed(
() => props.hasError && !props.hasMoreTokens
);
const getTokenExpiryText = (tokenExpiresOn: string | null) => {
if (!tokenExpiresOn) {
return t('infra_tokens.no_expiration');
}
const isTokenExpired =
new Date(tokenExpiresOn).toISOString() > tokenExpiresOn;
return isTokenExpired
? t('infra_tokens.expired')
: shortDateTime(tokenExpiresOn, false);
};
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div class="px-4 py-6 space-y-4 flex flex-col items-start">
<div class="space-y-1">
<h4 class="font-bold text-secondaryDark heading mt-2">
{{ t('infra_tokens.section_title') }}
</h4>
<p class="text-secondaryLight">
{{ t('infra_tokens.section_description') }}
</p>
</div>
<HoppButtonSecondary
filled
outline
:label="t('infra_tokens.generate_new_token')"
@click="emit('show-infra-tokens-generate-modal')"
/>
</div>
</template>
<script setup lang="ts">
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const emit = defineEmits<{
(e: 'show-infra-tokens-generate-modal'): void;
}>();
</script>

View File

@@ -0,0 +1,188 @@
<template>
<TokensOverview
@show-infra-tokens-generate-modal="showInfraTokensGenerateModal = true"
/>
<TokensList
:infra-tokens="infraTokens"
:has-error="tokensListFetchErrored"
:has-more-tokens="hasMoreTokens"
:loading="tokensListLoading"
@delete-infra-token="displayDeleteInfraTokenConfirmationModal"
@fetch-more-tokens="fetchMoreInfraTokens"
/>
<TokensGenerateModal
v-if="showInfraTokensGenerateModal"
:infra-token="infraToken"
:token-generate-action-loading="tokenGenerateActionLoading"
@generate-infra-token="generateInfraToken"
@hide-modal="hideInfraTokenGenerateModal"
/>
<HoppSmartConfirmModal
:show="confirmDeleteInfraToken"
:loading-state="tokenDeleteActionLoading"
:title="
t('state.confirm_delete_infra_token', {
tokenLabel: tokenToDelete?.label,
})
"
@hide-modal="confirmDeleteInfraToken = false"
@resolve="deleteInfraToken"
/>
</template>
<script setup lang="ts">
import { useMutation } from '@urql/vue';
import { Ref, ref, watch } from 'vue';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { usePagedQuery } from '~/composables/usePagedQuery';
import {
CreateInfraTokenDocument,
InfraTokensDocument,
InfraTokensQuery,
RevokeInfraTokenDocument,
} from '~/helpers/backend/graphql';
import { getCompiledErrorMessage } from '~/helpers/errors';
const t = useI18n();
const toast = useToast();
const confirmDeleteInfraToken = ref(false);
const hasMoreTokens = ref(false);
const showInfraTokensGenerateModal = ref(false);
const tokenDeleteActionLoading = ref(false);
const tokenGenerateActionLoading = ref(false);
const tokenToDelete = ref<{ id: string; label: string } | null>(null);
const infraToken: Ref<string | null> = ref(null);
const infraTokens: Ref<InfraTokensQuery['infraTokens']> = ref([]);
let offset = 0;
const tokensPerPage = 12;
const {
fetching: tokensListLoading,
error: tokensListFetchErrored,
list: tokensList,
refetch,
} = usePagedQuery(InfraTokensDocument, (x) => x.infraTokens, tokensPerPage, {
skip: offset,
take: tokensPerPage,
});
const fetchMoreInfraTokens = async () =>
await refetch({ skip: offset, take: tokensPerPage });
// Update the infraTokens list whenever tokensList is fetched
watch(tokensListLoading, (fetching) => {
if (fetching) return;
else {
infraTokens.value.push(...tokensList.value);
if (tokensList.value.length > 0) offset += tokensList.value.length;
hasMoreTokens.value = tokensList.value.length === tokensPerPage;
}
});
const createInfraTokens = useMutation(CreateInfraTokenDocument);
const generateInfraToken = async ({
label,
expiryInDays,
}: {
label: string;
expiryInDays: number | null;
}) => {
tokenGenerateActionLoading.value = true;
const variables = {
label,
expiryInDays,
};
const result = await createInfraTokens.executeMutation(variables);
if (result.error) {
const { message } = result.error;
const compiledErrorMessage = getCompiledErrorMessage(message);
compiledErrorMessage
? toast.error(t(compiledErrorMessage))
: toast.error(t('state.generate_infra_token_failure'));
showInfraTokensGenerateModal.value = false;
} else {
infraTokens.value.unshift(result.data!.createInfraToken.info);
infraToken.value = result.data!.createInfraToken.token;
offset += 1;
if (tokensListFetchErrored.value) {
tokensListFetchErrored.value = false;
}
}
tokenGenerateActionLoading.value = false;
};
const revokeInfraToken = useMutation(RevokeInfraTokenDocument);
const deleteInfraToken = async () => {
if (tokenToDelete.value === null) {
toast.error(t('state.something_went_wrong'));
return;
}
const { id: tokenIdToDelete, label: tokenLabelToDelete } =
tokenToDelete.value;
tokenDeleteActionLoading.value = true;
const result = await revokeInfraToken.executeMutation({
id: tokenIdToDelete,
});
if (result.error) {
toast.error(t('state.delete_infra_token_failure'));
} else {
infraTokens.value = infraTokens.value.filter(
(token) => token.id !== tokenIdToDelete
);
offset = offset > 0 ? offset - 1 : offset;
toast.success(
t('infra_tokens.deletion_success', { label: tokenLabelToDelete })
);
if (tokensListFetchErrored.value) {
tokensListFetchErrored.value = false;
}
}
tokenDeleteActionLoading.value = false;
confirmDeleteInfraToken.value = false;
tokenToDelete.value = null;
};
const hideInfraTokenGenerateModal = () => {
// Reset the reactive state variable holding infra token value and hide the modal
infraToken.value = null;
showInfraTokensGenerateModal.value = false;
};
const displayDeleteInfraTokenConfirmationModal = ({
tokenId,
tokenLabel,
}: {
tokenId: string;
tokenLabel: string;
}) => {
confirmDeleteInfraToken.value = true;
tokenToDelete.value = {
id: tokenId,
label: tokenLabel,
};
};
</script>

View File

@@ -0,0 +1,12 @@
mutation CreateInfraToken($label: String!, $expiryInDays: Int) {
createInfraToken(label: $label, expiryInDays: $expiryInDays) {
info {
id
label
lastUsedOn
createdOn
expiresOn
}
token
}
}

View File

@@ -0,0 +1,3 @@
mutation RevokeInfraToken($id: ID!) {
revokeInfraToken(id: $id)
}

View File

@@ -0,0 +1,9 @@
query InfraTokens($skip: Int, $take: Int) {
infraTokens(skip: $skip, take: $take) {
id
label
createdOn
lastUsedOn
expiresOn
}
}

View File

@@ -36,6 +36,9 @@ export const AUTH_PROVIDER_NOT_SPECIFIED =
export const BOTH_EMAILS_CANNOT_BE_SAME =
'[GraphQL] email/both_emails_cannot_be_same' as const;
export const INFRA_TOKEN_LABEL_SHORT =
'[GraphQL] infra_token/label_too_short' as const;
type ErrorMessages = {
message: string;
alternateMessage?: string;
@@ -68,6 +71,9 @@ const ERROR_MESSAGES: Record<string, ErrorMessages> = {
[BOTH_EMAILS_CANNOT_BE_SAME]: {
message: 'state.emails_cannot_be_same',
},
[INFRA_TOKEN_LABEL_SHORT]: {
message: 'state.infra_token_label_short',
},
};
export const getCompiledErrorMessage = (name: string, altMessage = false) => {

View File

@@ -0,0 +1,17 @@
export function shortDateTime(
date: string | number | Date,
includeTime: boolean = true
) {
return new Date(date).toLocaleString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
...(includeTime
? {
hour: "numeric",
minute: "numeric",
second: "numeric",
}
: {}),
})
}

View File

@@ -24,6 +24,9 @@
class="py-8 px-4"
/>
</HoppSmartTab>
<HoppSmartTab :id="'token'" :label="t('infra_tokens.tab_title')">
<Tokens />
</HoppSmartTab>
</HoppSmartTabs>
</div>
@@ -62,7 +65,7 @@ const showSaveChangesModal = ref(false);
const initiateServerRestart = ref(false);
// Tabs
type OptionTabs = 'config';
type OptionTabs = 'config' | 'token';
const selectedOptionTab = ref<OptionTabs>('config');
// Obtain the current and working configs from the useConfigHandler composable