From 2cb629ba1f6133d07a6824f29528d51c8f24504b Mon Sep 17 00:00:00 2001 From: Julius Date: Sun, 5 May 2024 22:59:01 +0200 Subject: [PATCH 01/97] display snapshots on homepage --- app/components/additional-snapshot-info.hbs | 44 ++++++ app/components/additional-snapshot-info.ts | 55 +++++++ app/components/delete-snapshot.hbs | 14 ++ app/components/delete-snapshot.ts | 26 ++++ app/components/share-snapshot.hbs | 14 ++ app/components/share-snapshot.ts | 30 ++++ app/components/snapshot-selection.hbs | 129 ++++++++++++++++ app/components/snapshot-selection.ts | 71 +++++++++ app/controllers/landscapes.ts | 17 +++ app/routes/landscapes.ts | 20 ++- app/services/snapshot-token.ts | 138 ++++++++++++++++++ app/templates/landscapes.hbs | 15 +- .../addon/components/room-list.hbs | 2 +- .../components/delete-snapshot-test.ts | 26 ++++ .../components/share-snapshot-test.ts | 26 ++++ .../components/snapshot-selection-test.ts | 26 ++++ tests/unit/services/snapshot-token-test.ts | 12 ++ 17 files changed, 657 insertions(+), 8 deletions(-) create mode 100644 app/components/additional-snapshot-info.hbs create mode 100644 app/components/additional-snapshot-info.ts create mode 100644 app/components/delete-snapshot.hbs create mode 100644 app/components/delete-snapshot.ts create mode 100644 app/components/share-snapshot.hbs create mode 100644 app/components/share-snapshot.ts create mode 100644 app/components/snapshot-selection.hbs create mode 100644 app/components/snapshot-selection.ts create mode 100644 app/services/snapshot-token.ts create mode 100644 tests/integration/components/delete-snapshot-test.ts create mode 100644 tests/integration/components/share-snapshot-test.ts create mode 100644 tests/integration/components/snapshot-selection-test.ts create mode 100644 tests/unit/services/snapshot-token-test.ts diff --git a/app/components/additional-snapshot-info.hbs b/app/components/additional-snapshot-info.hbs new file mode 100644 index 000000000..f5c18070f --- /dev/null +++ b/app/components/additional-snapshot-info.hbs @@ -0,0 +1,44 @@ + \ No newline at end of file diff --git a/app/components/additional-snapshot-info.ts b/app/components/additional-snapshot-info.ts new file mode 100644 index 000000000..54c289717 --- /dev/null +++ b/app/components/additional-snapshot-info.ts @@ -0,0 +1,55 @@ +import Component from '@glimmer/component'; +import ToastHandlerService from 'explorviz-frontend/services/toast-handler'; +import Auth from 'explorviz-frontend/services/auth'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { SnapshotToken } from 'explorviz-frontend/services/snapshot-token'; + +export default class AdditionalSnapshotInfoComponent extends Component { + @service('auth') + auth!: Auth; + + @service('toast-handler') + toastHandlerService!: ToastHandlerService; + + focusedClicks = 0; + + @action + // eslint-disable-next-line class-methods-use-this + onTokenIdCopied() { + this.toastHandlerService.showSuccessToastMessage( + 'Token id copied to clipboard' + ); + } + + @action + hidePopover(event: Event) { + if (this.isMouseOnPopover()) { + return; + } + + // Clicks enable us to differentiate between opened and closed popovers + if (this.focusedClicks % 2 === 1) { + event.target?.dispatchEvent(new Event('click')); + } + this.focusedClicks = 0; + } + + isMouseOnPopover() { + const hoveredElements = document.querySelectorAll(':hover'); + + for (const element of hoveredElements) { + if (element.matches('.popover')) { + return true; + } + } + return false; + } + + @action + onClick(event: Event) { + this.focusedClicks += 1; + // Prevent click on table row which would trigger to open the visualization + event.stopPropagation(); + } +} diff --git a/app/components/delete-snapshot.hbs b/app/components/delete-snapshot.hbs new file mode 100644 index 000000000..9333d7fde --- /dev/null +++ b/app/components/delete-snapshot.hbs @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/app/components/delete-snapshot.ts b/app/components/delete-snapshot.ts new file mode 100644 index 000000000..2763fca49 --- /dev/null +++ b/app/components/delete-snapshot.ts @@ -0,0 +1,26 @@ +import Component from '@glimmer/component'; + +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import Auth from 'explorviz-frontend/services/auth'; +import ToastHandlerService from 'explorviz-frontend/services/toast-handler'; +import { SnapshotToken } from 'explorviz-frontend/services/snapshot-token'; + +export default class DeleteSnapshotComponent extends Component { + @service('auth') + auth!: Auth; + + @service('toast-handler') + toastHandlerService!: ToastHandlerService; + + /** + * TODO: async function to delete Snapshot -> DB call + */ + @action + deleteSnapshot(snapShot: SnapshotToken) { + console.log('Delete' + snapShot.name); + this.toastHandlerService.showSuccessToastMessage( + 'Snapshot successfully deleted' + ); + } +} diff --git a/app/components/share-snapshot.hbs b/app/components/share-snapshot.hbs new file mode 100644 index 000000000..8e768b0ab --- /dev/null +++ b/app/components/share-snapshot.hbs @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/app/components/share-snapshot.ts b/app/components/share-snapshot.ts new file mode 100644 index 000000000..526163242 --- /dev/null +++ b/app/components/share-snapshot.ts @@ -0,0 +1,30 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import ToastHandlerService from 'explorviz-frontend/services/toast-handler'; +import Auth from 'explorviz-frontend/services/auth'; +import { SnapshotToken } from 'explorviz-frontend/services/snapshot-token'; + +/** + * TODO: website must reload to show snapshot in right table + * TODO: Button not only share, also revoke access or something like that + */ +export default class ShareSnapshotComponent extends Component { + @service('auth') + auth!: Auth; + + @service('toast-handler') + toastHandlerService!: ToastHandlerService; + + // this needs to be async to make DB call + /** + * TODO: create URL and edit DB entry + */ + @action + async shareSnapshot(snapShot: SnapshotToken) { + await navigator.clipboard.writeText(snapShot.landscapeToken.value); + this.toastHandlerService.showSuccessToastMessage( + 'URL to share snapshot copied to clipboard' + ); + } +} diff --git a/app/components/snapshot-selection.hbs b/app/components/snapshot-selection.hbs new file mode 100644 index 000000000..88a03d76e --- /dev/null +++ b/app/components/snapshot-selection.hbs @@ -0,0 +1,129 @@ +
+
Personal Snapshots
+
+ + + + + + + + + + {{#each + (sort-by + (concat this.sortPropertyPersonal ':' this.sortOrderPersonal) + 'createdAt' + (this.filter @snapShotTokens false) + ) + as |personalToken| + }} + + + + + + {{else}} + There are no saved snapshots. + {{/each}} + + + + +
AliasCreated
{{personalToken.name}} {{timestamp-to-date personalToken.createdAt 'localString'}} +
    +
  • + +
  • + +
  • + +
  • +
  • + +
  • +
+
+
+ + {{svg-jar 'plus-16' class='octicon'}} + +
+
+
+
+ +
+
Shared Snapshots
+
+ + + + + + + + + + {{#each + (sort-by + (concat this.sortPropertyShared ':' this.sortOrderShared) + 'createdAt' + (this.filter @snapShotTokens true) + ) + as |sharedToken| + }} + + + + + + {{else}} + There are no shared snapshots. + {{/each}} + +
AliasCreated
{{sharedToken.name}}{{timestamp-to-date sharedToken.createdAt 'localString'}} +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
\ No newline at end of file diff --git a/app/components/snapshot-selection.ts b/app/components/snapshot-selection.ts new file mode 100644 index 000000000..c17824167 --- /dev/null +++ b/app/components/snapshot-selection.ts @@ -0,0 +1,71 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import Auth from 'explorviz-frontend/services/auth'; +import { SnapshotToken } from 'explorviz-frontend/services/snapshot-token'; + +interface Args { + tokens: SnapshotToken[]; + selectToken(token: SnapshotToken): void; +} + +export default class SnapshotSelection extends Component { + @service('auth') + auth!: Auth; + + @tracked + sortPropertyPersonal: keyof SnapshotToken = 'createdAt'; + + @tracked + sortPropertyShared: keyof SnapshotToken = 'createdAt'; + + @tracked + sortOrderPersonal: 'asc' | 'desc' = 'asc'; + + @tracked + sortOrderShared: 'asc' | 'desc' = 'asc'; + + @action + sortByPersonal(property: keyof SnapshotToken) { + if (property === this.sortPropertyPersonal) { + if (this.sortOrderPersonal === 'asc') { + this.sortOrderPersonal = 'desc'; + } else { + this.sortOrderPersonal = 'asc'; + } + } else { + this.sortOrderPersonal = 'asc'; + this.sortPropertyPersonal = property; + } + } + + @action + sortByShared(property: keyof SnapshotToken) { + if (property === this.sortPropertyShared) { + if (this.sortOrderShared === 'asc') { + this.sortOrderShared = 'desc'; + } else { + this.sortOrderShared = 'asc'; + } + } else { + this.sortOrderShared = 'asc'; + this.sortPropertyShared = property; + } + } + + filter(snapShotTokens: SnapshotToken[], property: boolean) { + return snapShotTokens.filter((token) => token.isShared === property); + } + + // shoud be not used + // @action + // snapShotSelected() { + // console.log('snapShot selected'); + // } + + @action + uploadSnapshot() { + console.log('snapshot upload!'); + } +} diff --git a/app/controllers/landscapes.ts b/app/controllers/landscapes.ts index 44a87594f..51b240e95 100644 --- a/app/controllers/landscapes.ts +++ b/app/controllers/landscapes.ts @@ -8,6 +8,9 @@ import Auth from 'explorviz-frontend/services/auth'; import ToastHandlerService from 'explorviz-frontend/services/toast-handler'; import ENV from 'explorviz-frontend/config/environment'; import { tracked } from '@glimmer/tracking'; +import SnapshotTokenService, { + SnapshotToken, +} from 'explorviz-frontend/services/snapshot-token'; const { userService } = ENV.backendAddresses; @@ -15,6 +18,9 @@ export default class Landscapes extends Controller { @service('landscape-token') tokenService!: LandscapeTokenService; + @service('snapshot-token') + snapShotTokenService!: SnapshotTokenService; + @service('router') router!: any; @@ -50,6 +56,17 @@ export default class Landscapes extends Controller { }); } + /** + * TODO: update to load the highlighting as well + */ + @action + selectSnapshot(token: SnapshotToken) { + this.tokenService.setToken(token.landscapeToken); + this.router.transitionTo('visualization', { + queryParams: { landscapeToken: token.landscapeToken.value }, + }); + } + @action async createToken() { try { diff --git a/app/routes/landscapes.ts b/app/routes/landscapes.ts index e0d56fbff..770e7a112 100644 --- a/app/routes/landscapes.ts +++ b/app/routes/landscapes.ts @@ -5,6 +5,10 @@ import LandscapeTokenService, { import ENV from 'explorviz-frontend/config/environment'; import { action } from '@ember/object'; import BaseRoute from './base-route'; +import SnapshotTokenService, { + SnapshotToken, +} from 'explorviz-frontend/services/snapshot-token'; +import Ember from 'ember'; const { tokenToShow } = ENV.mode; @@ -12,6 +16,9 @@ export default class Landscapes extends BaseRoute { @service('landscape-token') tokenService!: LandscapeTokenService; + @service('snapshot-token') + snapshotService!: SnapshotTokenService; + @service('router') router!: any; @@ -25,12 +32,19 @@ export default class Landscapes extends BaseRoute { this.router.transitionTo('visualization'); } - return this.tokenService.retrieveTokens(); + //return this.tokenService.retrieveTokens(); + return Ember.RSVP.hash({ + landscapeTokens: this.tokenService.retrieveTokens(), + snapshotTokens: this.snapshotService.retrieveTokens(), + }); } - afterModel(landscapeTokens: LandscapeToken[]) { + afterModel(tokens: { + landscapeTokens: LandscapeToken[]; + snapshotTokens: SnapshotToken[]; + }) { const currentToken = this.tokenService.token; - const tokenCandidates = landscapeTokens.filter( + const tokenCandidates = tokens.landscapeTokens.filter( (token) => token.value === currentToken?.value ); if (tokenCandidates.length > 0) { diff --git a/app/services/snapshot-token.ts b/app/services/snapshot-token.ts new file mode 100644 index 000000000..917918007 --- /dev/null +++ b/app/services/snapshot-token.ts @@ -0,0 +1,138 @@ +import Service, { inject as service } from '@ember/service'; +// import { tracked } from '@glimmer/tracking'; +// import ENV from 'explorviz-frontend/config/environment'; +import Auth from './auth'; +import ToastHandlerService from './toast-handler'; +import { LandscapeToken } from './landscape-token'; + +export type SnapshotToken = { + id: number; + name: string; + landscapeToken: LandscapeToken; + owner: string; + createdAt: number; + configuration: any; + camera: any; + annotation: any; + isShared: boolean; + deleteAt?: number; +}; + +//const { userService } = ENV.backendAddresses; + +export default class SnapshotTokenService extends Service { + @service('auth') + private auth!: Auth; + + @service('toast-handler') + toastHandler!: ToastHandlerService; + + retrieveTokens(): SnapshotToken[] { + //uncomment when backend is implemented + + // return new Promise((resolve, reject) => { + // const userId = encodeURI(this.auth.user?.sub || ''); + // if (!userId) { + // resolve([]); + // } + + // fetch(`${userService}/user/${userId}/snapshottoken`, { + // headers: { + // Authorization: `Bearer ${this.auth.accessToken}`, + // }, + // }) + // .then(async (response: Response) => { + // if (response.ok) { + // const tokens = (await response.json()) as SnapShotToken[]; + // resolve(tokens); + // } else { + // reject(); + // } + // }) + // .catch(async (e) => { + // reject(e); + // }); + // }); + + return [ + { + id: 1, + name: 'firstSnapshotToken', + landscapeToken: { + alias: 'egal', + created: 123456, + ownerId: 'egal', + sharedUsersIds: [], + value: '12444195-6144-4254-a17b-asdgfewefg', + }, + owner: 'User', + createdAt: 1714833928, + configuration: null, + camera: null, + annotation: null, + isShared: false, + }, + { + id: 1, + name: 'secondSnapshotToken', + landscapeToken: { + alias: 'egal', + created: 123456, + ownerId: 'egal', + sharedUsersIds: [], + value: '12444195-6144-4254-a17b-asdgfewefg', + }, + owner: 'User', + createdAt: 1715833928, + configuration: null, + camera: null, + annotation: null, + isShared: false, + }, + { + id: 1, + name: 'firstSharedSnapshotToken', + landscapeToken: { + alias: 'egal', + created: 123456, + ownerId: 'egal', + sharedUsersIds: [], + value: '12444195-6144-4254-a17b-asdgfewefg', + }, + owner: 'User', + createdAt: 1716833928, + configuration: null, + camera: null, + annotation: null, + isShared: true, + }, + { + id: 1, + name: 'secondSharedSnapshotToken', + landscapeToken: { + alias: 'egal', + created: 123456, + ownerId: 'egal', + sharedUsersIds: [], + value: '12444195-6144-4254-a17b-asdgfewefg', + }, + owner: 'User', + createdAt: 1917833928, + configuration: null, + camera: null, + annotation: null, + isShared: true, + }, + ]; + } +} + +// Don't remove this declaration: this is what enables TypeScript to resolve +// this service using `Owner.lookup('service:snapshot-token')`, as well +// as to check when you pass the service name as an argument to the decorator, +// like `@service('snapshot-token') declare altName: SnapshotTokenService;`. +declare module '@ember/service' { + interface Registry { + 'snapshot-token': SnapshotTokenService; + } +} diff --git a/app/templates/landscapes.hbs b/app/templates/landscapes.hbs index b5f466b0f..16e2e35ff 100644 --- a/app/templates/landscapes.hbs +++ b/app/templates/landscapes.hbs @@ -1,17 +1,24 @@ -
+

Select Software Landscape

-
+

Open Sessions

- + +

Snapshots

+
+ +
+
diff --git a/tests/integration/components/delete-snapshot-test.ts b/tests/integration/components/delete-snapshot-test.ts new file mode 100644 index 000000000..3e4e0f899 --- /dev/null +++ b/tests/integration/components/delete-snapshot-test.ts @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'explorviz-frontend/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | delete-snapshot', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + await render(hbs``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +}); diff --git a/tests/integration/components/share-snapshot-test.ts b/tests/integration/components/share-snapshot-test.ts new file mode 100644 index 000000000..9806cdd7b --- /dev/null +++ b/tests/integration/components/share-snapshot-test.ts @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'explorviz-frontend/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | share-snapshot', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + await render(hbs``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +}); diff --git a/tests/integration/components/snapshot-selection-test.ts b/tests/integration/components/snapshot-selection-test.ts new file mode 100644 index 000000000..529a0dee1 --- /dev/null +++ b/tests/integration/components/snapshot-selection-test.ts @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'explorviz-frontend/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | snapshot-selection', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + await render(hbs``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +}); diff --git a/tests/unit/services/snapshot-token-test.ts b/tests/unit/services/snapshot-token-test.ts new file mode 100644 index 000000000..58db7c779 --- /dev/null +++ b/tests/unit/services/snapshot-token-test.ts @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'explorviz-frontend/tests/helpers'; + +module('Unit | Service | snapshot-token', function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let service = this.owner.lookup('service:snapshot-token'); + assert.ok(service); + }); +}); From 34fd5b5268645e1e76715a5050449bc14bf605a3 Mon Sep 17 00:00:00 2001 From: Julius Date: Sun, 5 May 2024 22:59:53 +0200 Subject: [PATCH 02/97] added settings page and display api tokens on settings page --- app/components/api-token-selection.hbs | 73 +++++++++++++++++++ app/components/api-token-selection.ts | 46 ++++++++++++ app/components/page-setup/navbar.hbs | 10 +++ app/components/page-setup/navbar.ts | 5 ++ app/router.js | 1 + app/routes/settings.ts | 15 ++++ app/services/user-api-token.ts | 72 ++++++++++++++++++ app/templates/settings.hbs | 6 ++ .../additional-snapshot-info-test.ts | 26 +++++++ .../components/api-token-selection-test.ts | 26 +++++++ .../snapshot/snapshot-opener-test.ts | 26 +++++++ tests/unit/routes/settings-test.ts | 11 +++ tests/unit/services/user-api-token-test.ts | 12 +++ 13 files changed, 329 insertions(+) create mode 100644 app/components/api-token-selection.hbs create mode 100644 app/components/api-token-selection.ts create mode 100644 app/routes/settings.ts create mode 100644 app/services/user-api-token.ts create mode 100644 app/templates/settings.hbs create mode 100644 tests/integration/components/additional-snapshot-info-test.ts create mode 100644 tests/integration/components/api-token-selection-test.ts create mode 100644 tests/integration/components/visualization/page-setup/sidebar/customizationbar/snapshot/snapshot-opener-test.ts create mode 100644 tests/unit/routes/settings-test.ts create mode 100644 tests/unit/services/user-api-token-test.ts diff --git a/app/components/api-token-selection.hbs b/app/components/api-token-selection.hbs new file mode 100644 index 000000000..1084371c2 --- /dev/null +++ b/app/components/api-token-selection.hbs @@ -0,0 +1,73 @@ +
+
API-Tokens
+
+
+ + + + + + + + + + {{#each + (sort-by + (concat this.sortProperty ':' this.sortOrder) 'name' @apiTokens + ) + as |apiToken| + }} + + + + + + + {{else}} + There are no saved API-Tokens. + {{/each}} + + + + +
NameCreatedExpires
{{apiToken.name}} {{timestamp-to-date + apiToken.createdAt + 'localString' + }}{{timestamp-to-date + apiToken.expires + 'localString' + }} + +
+
+ + {{svg-jar 'plus-16' class='octicon'}} + +
+
+
+
\ No newline at end of file diff --git a/app/components/api-token-selection.ts b/app/components/api-token-selection.ts new file mode 100644 index 000000000..368243a86 --- /dev/null +++ b/app/components/api-token-selection.ts @@ -0,0 +1,46 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import Auth from 'explorviz-frontend/services/auth'; +import { tracked } from '@glimmer/tracking'; +import { ApiToken } from 'explorviz-frontend/services/user-api-token'; + +export default class ApiTokenSelectionComponent extends Component { + @service + auth!: Auth; + + @tracked + sortProperty: keyof ApiToken = 'name'; + + @tracked + sortOrder: 'asc' | 'desc' = 'asc'; + + @action + sortBy(property: keyof ApiToken) { + if (property === this.sortProperty) { + if (this.sortOrder === 'asc') { + this.sortOrder = 'desc'; + } else { + this.sortOrder = 'asc'; + } + } else { + this.sortOrder = 'asc'; + this.sortProperty = property; + } + } + + /* + * TODO: deleteApiTOken from DB + * @param apiToken + */ + @action + deleteApiToken(apiToken: string) { + // give db uID wiht auth.user.user_id or if not auth ("johnny", than the hardcoded uId) + console.log(apiToken); + } + + @action + createApiToken() { + console.log('Create API-Token'); + } +} diff --git a/app/components/page-setup/navbar.hbs b/app/components/page-setup/navbar.hbs index 37c5c3bba..2420b6e41 100644 --- a/app/components/page-setup/navbar.hbs +++ b/app/components/page-setup/navbar.hbs @@ -72,6 +72,16 @@ }} {{this.auth.user.sub}}
+
  • + +
  • {{#if this.tokenService.token}}