Skip to content

Commit

Permalink
feat: add the ability for creating/deleting PATs from the UI
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesgeorge007 committed Jun 1, 2024
1 parent 4bd23a8 commit acbea3a
Show file tree
Hide file tree
Showing 9 changed files with 589 additions and 19 deletions.
27 changes: 25 additions & 2 deletions packages/hoppscotch-common/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress.",
"delete_access_token": "Are you sure you want to delete the access token {tokenLabel}?"
},
"context_menu": {
"add_parameters": "Add to parameters",
Expand Down Expand Up @@ -342,7 +343,10 @@
"script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong",
"test_script_fail": "Could not execute post-request script",
"reading_files": "Error while reading one or more files."
"reading_files": "Error while reading one or more files.",
"fetching_access_tokens_list": "Something went wrong while fetching the list of tokens",
"generate_access_token": "Something went wrong while generating the access token",
"delete_access_token": "Something went wrong while deleting the access token"
},
"export": {
"as_json": "Export as JSON",
Expand Down Expand Up @@ -1033,5 +1037,24 @@
"login_to_continue": "Login to continue",
"login_to_continue_description": "You need to be logged in to access this Hoppscotch Enterprise Instance.",
"error_fetching_site_protection_status": "Something Went Wrong While Fetching Site Protection Status"
},
"access_tokens": {
"tab_title": "Tokens",
"section_title": "Personal Access Tokens",
"section_description": "Personal access tokens currently helps you connect the CLI to your Hoppscotch account",
"last_used_on": "Last used on",
"expires_on": "Expires on",
"no_expiration": "No expiration",
"copy_token_warning": "Make sure to copy your personal access token now. You won't be able to see it again!",
"token_purpose": "What's this token for?",
"expiration_label": "Expiration",
"scope_label": "Scope",
"workspace_read_only_access": "Read-only access to workspace data.",
"personal_workspace_access_limitation": "Personal Access Tokens can't access your personal workspace.",
"generate_token": "Generate Token",
"invalid_label": "Please provide a label for the token",
"no_expiration_verbose": "This token will never expire!",
"token_expires_on": "This token will expire on",
"generate_new_token": "Generate new token"
}
}
10 changes: 8 additions & 2 deletions packages/hoppscotch-common/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export {}

declare module 'vue' {
export interface GlobalComponents {
AccessTokens: typeof import('./components/accessTokens/index.vue')['default']
AccessTokensGenerate: typeof import('./components/accessTokens/Generate.vue')['default']
AccessTokensGenerateModal: typeof import('./components/accessTokens/GenerateModal.vue')['default']
AccessTokensList: typeof import('./components/accessTokens/List.vue')['default']
AccessTokensOverview: typeof import('./components/accessTokens/Overview.vue')['default']
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
AppBanner: typeof import('./components/app/Banner.vue')['default']
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
Expand Down Expand Up @@ -148,7 +153,7 @@ declare module 'vue' {
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideBrush: (typeof import("~icons/lucide/brush"))["default"]
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
Expand All @@ -158,9 +163,10 @@ declare module 'vue' {
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucideRss: (typeof import("~icons/lucide/rss"))["default"]
IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default']
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
IconLucideX: typeof import('~icons/lucide/x')['default']
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<template>
<HoppSmartModal dialog title="New Personal Access Token" @close="hideModal">
<template #body>
<template v-if="accessToken">
<p class="p-4 mb-4 border rounded-md text-amber-500 border-amber-600">
{{ t("access_tokens.copy_token_warning") }}
</p>

<div
class="flex items-center justify-between p-4 mt-4 rounded-md bg-primaryLight"
>
<div class="text-secondaryDark">{{ accessToken }}</div>
<HoppButtonSecondary
outline
filled
:icon="copyIcon"
@click="copyAccessToken"
/>
</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="accessTokenLabel"
placeholder=" "
class="floating-input"
/>
<div class="text-secondaryLight">
{{ t("access_tokens.token_purpose") }}
</div>
</div>

<div class="space-y-2">
<label for="expiration" class="font-semibold text-secondaryDark">{{
t("access_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 class="space-y-2">
<div class="font-semibold text-secondaryDark">
{{ t("access_tokens.scope_label") }}
</div>

<p class="text-secondaryLight">
{{ t("access_tokens.workspace_read_only_access") }}<br />
{{ t("access_tokens.personal_workspace_access_limitation") }}
</p>
</div>
</div>
</template>
<template #footer>
<HoppButtonSecondary
v-if="accessToken"
: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('access_tokens.generate_token')"
@click="generateAccessToken"
/>

<HoppButtonSecondary
:label="t('action.cancel')"
outline
filled
@click="hideModal"
/>
</div>
</template>
</HoppSmartModal>
</template>

<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { refAutoReset } from "@vueuse/core"
import { computed, ref } from "vue"
import { TippyComponent } from "vue-tippy"
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
accessToken: string | null
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
(
e: "generate-access-token",
{ label, expiryInDays }: { label: string; expiryInDays: number | null }
): void
}>()
// Template refs
const tippyActions = ref<TippyComponent[] | null>(null)
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const accessTokenLabel = 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("access_tokens.no_expiration_verbose")
}
const currentDate = new Date()
currentDate.setDate(currentDate.getDate() + chosenExpiryInDays)
const expirationDate = shortDateTime(currentDate, false)
return `${t("access_tokens.token_expires_on")} ${expirationDate}`
})
const copyAccessToken = () => {
if (!props.accessToken) {
toast.error("error.something_went_wrong")
return
}
copyToClipboard(props.accessToken)
copyIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const generateAccessToken = async () => {
if (!accessTokenLabel.value) {
toast.error(t("access_tokens.invalid_label"))
return
}
emit("generate-access-token", {
label: accessTokenLabel.value,
expiryInDays: expirationOptions[expiration.value],
})
}
const hideModal = () => emit("hide-modal")
</script>
82 changes: 82 additions & 0 deletions packages/hoppscotch-common/src/components/accessTokens/List.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<template>
<div class="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<div
v-for="{ id, label, lastUsedOn, expiresOn } in accessTokens"
:key="id"
class="flex items-center justify-between p-4 border rounded md:items-baseline md:flex-col md:gap-4 md:justify-normal border-divider"
>
<div class="w-full text-sm font-semibold truncate text-secondaryDark">
{{ label }}
</div>

<div class="flex items-center gap-x-4">
<div class="space-y-1 text-secondaryLight">
<div class="space-x-1">
<span class="font-semibold"
>{{ t("access_tokens.last_used_on") }}:</span
>
<span>
{{ shortDateTime(lastUsedOn, false) }}
</span>
</div>

<div class="space-x-1">
<span class="font-semibold"
>{{ t("access_tokens.expires_on") }}:</span
>
<span>
{{
expiresOn
? shortDateTime(expiresOn, false)
: t("access_tokens.no_expiration")
}}
</span>
</div>
</div>

<HoppButtonSecondary
:label="t('action.delete')"
filled
outline
@click="
emit('delete-access-token', {
tokenId: id,
tokenLabel: label,
})
"
/>
</div>
</div>
</div>

<HoppSmartIntersection
v-if="hasMoreTokens"
@intersecting="emit('fetch-more-tokens')"
>
<div v-if="tokensListLoading" class="flex flex-col items-center py-3">
<HoppSmartSpinner />
</div>
</HoppSmartIntersection>
</template>

<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { shortDateTime } from "~/helpers/utils/date"
import { AccessToken } from "~/pages/profile.vue"
const t = useI18n()
defineProps<{
accessTokens: AccessToken[]
hasMoreTokens: boolean
tokensListLoading: boolean
}>()
const emit = defineEmits<{
(e: "fetch-more-tokens"): void
(
e: "delete-access-token",
{ tokenId, tokenLabel }: { tokenId: string; tokenLabel: string }
): void
}>()
</script>
Loading

0 comments on commit acbea3a

Please sign in to comment.