Skip to content

Commit

Permalink
feat: delete app without need to delete ingress rules (#238)
Browse files Browse the repository at this point in the history
  • Loading branch information
tanmoysrt authored Jun 27, 2024
1 parent 4b58ec1 commit 3805483
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 47 deletions.
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@apollo/client": "^3.8.8",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.5",
"@headlessui/vue": "^1.7.16",
Expand Down
4 changes: 3 additions & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
faClipboard,
faCloud,
faCodeBranch,
faCodeCommit,
faCopy,
faCubesStacked,
faDiagramProject,
Expand Down Expand Up @@ -85,7 +86,6 @@ import {
faUserTie,
faVault,
faWrench,
faCodeCommit,
faXmark
} from '@fortawesome/free-solid-svg-icons'

Expand All @@ -103,6 +103,7 @@ import { createClient } from 'graphql-ws'
import { GraphQLWsLink } from '@apollo/client/link/subscriptions' // <-- This one uses graphql-ws
import { getMainDefinition } from '@apollo/client/utilities'
import { getGraphQlHttpBaseUrl, getGraphQlWsBaseUrl } from '@/vendor/utils.js'
import { faCircle } from '@fortawesome/free-regular-svg-icons/faCircle'

// add icons to library
library.add(
Expand Down Expand Up @@ -183,6 +184,7 @@ library.add(
faEnvelope,
faPeopleGroup,
faLayerGroup,
faCircle,
faCircleNotch,
faPersonDigging,
faShieldHalved,
Expand Down
55 changes: 9 additions & 46 deletions src/views/pages/ApplicationDetails/Destroy.vue
Original file line number Diff line number Diff line change
@@ -1,76 +1,39 @@
<script setup>
import { useRouter } from 'vue-router'
import FilledButton from '@/views/components/FilledButton.vue'
import { useMutation } from '@vue/apollo-composable'
import gql from 'graphql-tag'
import { useToast } from 'vue-toastification'
import DeleteApplicationsModal from '@/views/partials/DeleteApplicationsModal.vue'
import { ref } from 'vue'
const router = useRouter()
const toast = useToast()
const {
mutate: deleteApplication,
loading: deleteApplicationLoading,
onError: deleteApplicationError,
onDone: deleteApplicationDone
} = useMutation(gql`
mutation ($id: String!) {
deleteApplication(id: $id)
}
`)
const deleteApplicationsModal = ref(null)
function deleteApplicationWithConfirmation() {
// ask to type `delete` to confirm
const confirmation = prompt('Type `delete` to confirm')
if (confirmation === 'delete') {
deleteApplication({
id: router.currentRoute.value.params.id
})
} else {
alert('Retry again !')
if (deleteApplicationsModal.value) {
deleteApplicationsModal.value.openModal()
}
}
deleteApplicationDone((result) => {
if (result.data.deleteApplication) {
toast.success('Application deleted successfully !')
router.push('/applications')
} else {
toast.error('Something went wrong !')
}
})
deleteApplicationError((error) => {
toast.error(error.message)
})
</script>

<template>
<DeleteApplicationsModal ref="deleteApplicationsModal" :application-ids="[router.currentRoute.value.params.id]" />
<div class="w-full rounded-md border border-warning-200 bg-warning-100 p-2">
Use the below options with caution. These actions are non-reversible.
</div>
<div class="mt-3 w-full rounded-md border border-danger-200 bg-danger-100 p-2">
<b>Note :</b> You need to delete all the ingress rules pointed to this application manually before deleting this
application.
</div>
<div class="mt-3 flex flex-col items-start">
<p class="font-bold text-danger-500">Do you like to delete this application ?</p>
<p class="mt-2">This action will remove these stuffs -</p>
<ul class="list-inside list-disc">
<li>Application</li>
<li>Ingress Rules</li>
<li>Related Deployments</li>
<li>Deployment Logs</li>
<li>Environment Variables</li>
<li>Persistent Volume Bindings</li>
<li>Uploaded Source Code</li>
</ul>

<FilledButton
class="mt-6"
type="danger"
:loading="deleteApplicationLoading"
:click="deleteApplicationWithConfirmation"
>Confirm & Delete Application
<FilledButton class="mt-6" type="danger" :click="deleteApplicationWithConfirmation"
>Delete Ingress Rules & Application
</FilledButton>
</div>
</template>
Expand Down
194 changes: 194 additions & 0 deletions src/views/partials/DeleteApplicationsModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<script setup>
import ModalDialog from '@/views/components/ModalDialog.vue'
import { ref } from 'vue'
import { useLazyQuery, useMutation } from '@vue/apollo-composable'
import gql from 'graphql-tag'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import FilledButton from '@/views/components/FilledButton.vue'
import { useToast } from 'vue-toastification'
import { useRouter } from 'vue-router'
const props = defineProps({
applicationIds: {
type: Array,
required: true
}
})
const toast = useToast()
const router = useRouter()
const isOpen = ref(false)
const openModal = () => {
isOpen.value = true
fetchAllAppDetails()
}
const closeModal = () => {
if (isDeleting.value) {
toast.error('Wait until application deletion is completed')
return
}
isLoadingApplicationDetailsFirstTime.value = true
applicationDetails.value = {}
isOpen.value = false
}
const { load: loadApplicationRaw, refetch: refetchApplicationRaw } = useLazyQuery(
gql`
query ($id: String!) {
application(id: $id) {
id
name
ingressRules {
id
}
}
}
`,
null,
{
fetchPolicy: 'no-cache',
nextFetchPolicy: 'no-cache'
}
)
const fetchLoadApplicationDetails = async (appId) => {
let res = await loadApplicationRaw(
null,
{
id: appId
},
null
)
if (!res) {
res = await refetchApplicationRaw({
id: appId
})
}
return res?.application ?? res?.data?.application ?? {}
}
const applicationDetails = ref({})
const isLoadingApplicationDetailsFirstTime = ref(true)
const isDeleting = ref(false)
const fetchAllAppDetails = async () => {
let data = {}
for (const appId of props.applicationIds) {
data[appId] = await fetchLoadApplicationDetails(appId)
}
applicationDetails.value = data
isLoadingApplicationDetailsFirstTime.value = false
return data
}
defineExpose({
openModal,
closeModal
})
// Delete ingress rule
const { mutate: deleteIngressRule } = useMutation(gql`
mutation ($id: Uint!) {
deleteIngressRule(id: $id)
}
`)
// Delete application
const { mutate: deleteApplication } = useMutation(gql`
mutation ($id: String!) {
deleteApplication(id: $id)
}
`)
const deleteApplications = async () => {
const confirmation = prompt('Type `delete` to confirm')
if (confirmation !== 'delete') {
return
}
isDeleting.value = true
while (isDeleting.value) {
const latestApplicationDetails = await fetchAllAppDetails()
let isIngressRuleDeletionRequired = false
// check if any ingress rules are using these applications
for (const appId of props.applicationIds) {
const app = latestApplicationDetails[appId]
if (app?.ingressRules?.length > 0) {
isIngressRuleDeletionRequired = true
break
}
}
if (isIngressRuleDeletionRequired) {
for (const appId of props.applicationIds) {
const app = latestApplicationDetails[appId]
for (const ingressRule of app?.ingressRules ?? []) {
if (ingressRule.status === 'deleting') continue
try {
await deleteIngressRule({
id: ingressRule.id
})
} catch (error) {
toast.error(error.message)
}
}
}
await new Promise((resolve) => setTimeout(resolve, 2000))
} else {
for (const appId of props.applicationIds) {
try {
await deleteApplication({
id: appId
})
} catch (error) {
toast.error(error.message)
}
}
toast.success(
props.applicationIds.length > 1
? 'Applications deletion initiated successfully ! It may take a few seconds to complete.'
: 'Application deletion initiated successfully ! It may take a few seconds to complete.'
)
isDeleting.value = false
closeModal()
router.push('/applications')
}
}
}
</script>
<template>
<ModalDialog :is-open="isOpen" :close-modal="closeModal">
<template v-slot:header>
<span>Delete Application<span v-if="props.applicationIds.length > 1">s</span></span>
</template>
<template v-slot:body>
<div class="mb-2 mt-4 w-full rounded-md border border-warning-200 bg-warning-100 p-2 text-sm">
Don't close this window until all the app<span v-if="props.applicationIds.length > 1">s</span> are deleted.
</div>
<p v-if="isLoadingApplicationDetailsFirstTime">Loading application details...</p>
<div v-else class="mt-2">
<div v-for="app in applicationDetails" :key="app.id" class="flex w-full flex-row items-center gap-2">
<font-awesome-icon
v-if="isDeleting"
icon="fa-solid fa-circle-notch"
class="animate-spin text-base text-warning-500" />
<font-awesome-icon v-else icon="fa-regular fa-circle" class="text-base text-warning-500" />
<span class="text-secondary-800"
>{{ app.name }}&nbsp;<span v-if="(app?.ingressRules ?? []).length > 0"
>({{ (app?.ingressRules ?? []).length }} Ingress Rules)</span
></span
>
</div>
</div>
</template>
<template v-slot:footer>
<FilledButton
type="danger"
class="w-full"
:click="deleteApplications"
:loading="isDeleting"
:disabled="isDeleting || isLoadingApplicationDetailsFirstTime"
>Delete Application<span v-if="props.applicationIds.length > 1">s</span></FilledButton
>
</template>
</ModalDialog>
</template>
<style scoped></style>

0 comments on commit 3805483

Please sign in to comment.