-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add the ability for creating/deleting PATs from the UI
- Loading branch information
1 parent
4bd23a8
commit 29614eb
Showing
8 changed files
with
588 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
221 changes: 221 additions & 0 deletions
221
packages/hoppscotch-common/src/components/accessTokens/GenerateModal.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
82
packages/hoppscotch-common/src/components/accessTokens/List.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.