PersonalSettingDevices.vue 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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/CommonPopoverMenu/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. id: 'device-revoked',
  74. type: NotificationTypes.Success,
  75. message: __('Device has been revoked.'),
  76. })
  77. })
  78. }
  79. const confirmDeleteDevice = async (device: UserDevice) => {
  80. const confirmed = await waitForVariantConfirmation('delete')
  81. if (confirmed) deleteDevice(device)
  82. }
  83. const tableHeaders: TableHeader[] = [
  84. {
  85. key: 'name',
  86. label: __('Name'),
  87. truncate: true,
  88. },
  89. {
  90. key: 'location',
  91. label: __('Location'),
  92. truncate: true,
  93. },
  94. {
  95. key: 'updatedAt',
  96. label: __('Most recent activity'),
  97. type: 'timestamp',
  98. },
  99. ]
  100. const tableActions: MenuItem[] = [
  101. {
  102. key: 'delete',
  103. label: __('Delete this device'),
  104. icon: 'trash3',
  105. variant: 'danger',
  106. show: (data) => !data?.current,
  107. onClick: (data) => {
  108. confirmDeleteDevice(data as UserDevice)
  109. },
  110. },
  111. ]
  112. const currentDevices = computed<TableItem[]>(() => {
  113. return (deviceListQueryResult.value?.userCurrentDeviceList || []).map(
  114. (device) => {
  115. return {
  116. ...device,
  117. current: device.fingerprint && device.fingerprint === fingerprint.value,
  118. }
  119. },
  120. )
  121. })
  122. const helpText = computed(() =>
  123. i18n.t(
  124. 'All computers and browsers from which you logged in to Zammad appear here.',
  125. ),
  126. )
  127. </script>
  128. <template>
  129. <LayoutContent
  130. :breadcrumb-items="breadcrumbItems"
  131. :help-text="helpText"
  132. width="narrow"
  133. provide-default
  134. >
  135. <CommonLoader :loading="deviceListQueryLoading">
  136. <div class="mb-4">
  137. <CommonSimpleTable
  138. :headers="tableHeaders"
  139. :items="currentDevices"
  140. :actions="tableActions"
  141. class="min-w-150"
  142. :aria-label="helpText"
  143. >
  144. <template #item-suffix-name="{ item }">
  145. <CommonBadge
  146. v-if="item.current"
  147. variant="info"
  148. class="ltr:ml-2 rtl:mr-2"
  149. >{{ $t('This device') }}
  150. </CommonBadge>
  151. </template>
  152. </CommonSimpleTable>
  153. </div>
  154. </CommonLoader>
  155. </LayoutContent>
  156. </template>