PersonalSettingDevices.vue 5.0 KB

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