PersonalSettingDevices.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. <!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed } from 'vue'
  4. import { NotificationTypes } from '#shared/components/CommonNotifications/types.ts'
  5. import { useNotifications } from '#shared/components/CommonNotifications/useNotifications.ts'
  6. import { useConfirmation } from '#shared/composables/useConfirmation.ts'
  7. import useFingerprint from '#shared/composables/useFingerprint.ts'
  8. import type {
  9. UserCurrentDevicesUpdatesSubscription,
  10. UserCurrentDevicesUpdatesSubscriptionVariables,
  11. UserCurrentDeviceListQuery,
  12. UserDevice,
  13. } from '#shared/graphql/types.ts'
  14. import { i18n } from '#shared/i18n/index.ts'
  15. import MutationHandler from '#shared/server/apollo/handler/MutationHandler.ts'
  16. import QueryHandler from '#shared/server/apollo/handler/QueryHandler.ts'
  17. import CommonLoader from '#desktop/components/CommonLoader/CommonLoader.vue'
  18. import type { MenuItem } from '#desktop/components/CommonPopoverMenu/types.ts'
  19. import CommonSimpleTable from '#desktop/components/CommonTable/CommonSimpleTable.vue'
  20. import type {
  21. TableSimpleHeader,
  22. TableItem,
  23. } from '#desktop/components/CommonTable/types.ts'
  24. import LayoutContent from '#desktop/components/layout/LayoutContent.vue'
  25. import { useBreadcrumb } from '../composables/useBreadcrumb.ts'
  26. import { useUserCurrentDeviceDeleteMutation } from '../graphql/mutations/userCurrentDeviceDelete.api.ts'
  27. import { useUserCurrentDeviceListQuery } from '../graphql/queries/userCurrentDeviceList.api.ts'
  28. import { UserCurrentDevicesUpdatesDocument } from '../graphql/subscriptions/userCurrentDevicesUpdates.api.ts'
  29. const { breadcrumbItems } = useBreadcrumb(__('Devices'))
  30. const { notify } = useNotifications()
  31. const { fingerprint } = useFingerprint()
  32. const deviceListQuery = new QueryHandler(useUserCurrentDeviceListQuery())
  33. const deviceListQueryResult = deviceListQuery.result()
  34. const deviceListQueryLoading = deviceListQuery.loading()
  35. deviceListQuery.subscribeToMore<
  36. UserCurrentDevicesUpdatesSubscriptionVariables,
  37. UserCurrentDevicesUpdatesSubscription
  38. >({
  39. document: UserCurrentDevicesUpdatesDocument,
  40. updateQuery: (prev, { subscriptionData }) => {
  41. if (!subscriptionData.data?.userCurrentDevicesUpdates.devices) {
  42. return null as unknown as UserCurrentDeviceListQuery
  43. }
  44. return {
  45. userCurrentDeviceList:
  46. subscriptionData.data.userCurrentDevicesUpdates.devices,
  47. }
  48. },
  49. })
  50. const { waitForVariantConfirmation } = useConfirmation()
  51. const deleteDevice = (device: UserDevice) => {
  52. const deviceDeleteMutation = new MutationHandler(
  53. useUserCurrentDeviceDeleteMutation(() => ({
  54. variables: {
  55. deviceId: device.id,
  56. },
  57. update(cache) {
  58. cache.evict({ id: cache.identify(device) })
  59. cache.gc()
  60. },
  61. })),
  62. {
  63. errorNotificationMessage: __('The device could not be deleted.'),
  64. },
  65. )
  66. deviceDeleteMutation.send().then(() => {
  67. notify({
  68. id: 'device-revoked',
  69. type: NotificationTypes.Success,
  70. message: __('Device has been revoked.'),
  71. })
  72. })
  73. }
  74. const confirmDeleteDevice = async (device: UserDevice) => {
  75. const confirmed = await waitForVariantConfirmation('delete')
  76. if (confirmed) deleteDevice(device)
  77. }
  78. const tableHeaders: TableSimpleHeader[] = [
  79. {
  80. key: 'name',
  81. label: __('Name'),
  82. truncate: true,
  83. },
  84. {
  85. key: 'location',
  86. label: __('Location'),
  87. truncate: true,
  88. },
  89. {
  90. key: 'updatedAt',
  91. label: __('Most recent activity'),
  92. type: 'timestamp',
  93. },
  94. ]
  95. const tableActions: MenuItem[] = [
  96. {
  97. key: 'delete',
  98. label: __('Delete this device'),
  99. icon: 'trash3',
  100. variant: 'danger',
  101. show: (data) => !data?.current,
  102. onClick: (data) => {
  103. confirmDeleteDevice(data as UserDevice)
  104. },
  105. },
  106. ]
  107. const currentDevices = computed<TableItem[]>(() => {
  108. return (deviceListQueryResult.value?.userCurrentDeviceList || []).map(
  109. (device) => {
  110. return {
  111. ...device,
  112. current: device.fingerprint && device.fingerprint === fingerprint.value,
  113. }
  114. },
  115. )
  116. })
  117. const helpText = computed(() =>
  118. i18n.t(
  119. 'All computers and browsers from which you logged in to Zammad appear here.',
  120. ),
  121. )
  122. </script>
  123. <template>
  124. <LayoutContent
  125. :breadcrumb-items="breadcrumbItems"
  126. :help-text="helpText"
  127. width="narrow"
  128. provide-default
  129. >
  130. <CommonLoader :loading="deviceListQueryLoading">
  131. <div class="mb-4">
  132. <CommonSimpleTable
  133. :caption="$t('Used devices')"
  134. :headers="tableHeaders"
  135. :items="currentDevices"
  136. :actions="tableActions"
  137. class="min-w-150"
  138. :aria-label="helpText"
  139. >
  140. <template #item-suffix-name="{ item }">
  141. <CommonBadge
  142. v-if="item.current"
  143. variant="info"
  144. class="ltr:ml-2 rtl:mr-2"
  145. >{{ $t('This device') }}
  146. </CommonBadge>
  147. </template>
  148. </CommonSimpleTable>
  149. </div>
  150. </CommonLoader>
  151. </LayoutContent>
  152. </template>