diff --git a/.env b/.env index 9b3684855..d194a083c 100644 --- a/.env +++ b/.env @@ -3,6 +3,9 @@ FRONTEND_URL=http://localhost:4200 COLLABORATION_SERV_URL=http://localhost:4444 SPAN_SERV_URL=http://localhost:8083 USER_SERV_URL=http://localhost:8084 +USER_SERV_API_URL=http://localhost:8080 +SHARE_SNAPSHOT_URL=http://localhost:4200/ +GITLAB_API=http://localhost:5000 CODE_SERV_URL=http://localhost:8085 METRICS_SERV_URL=http://localhost:8086 VSCODE_SERV_URL=http://localhost:3000 diff --git a/.eslintrc.js b/.eslintrc.js index f91e51dc8..474112ae9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,7 +6,12 @@ module.exports = { sourceType: 'module', project: './tsconfig.json', }, - plugins: ['@typescript-eslint', 'import', 'prettier'], + plugins: [ + // 'ember', + 'prettier', + '@typescript-eslint', + 'import', + ], extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', @@ -24,13 +29,18 @@ module.exports = { auth0: false, }, rules: { + 'prettier/prettier': 'error', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/ban-ts-comment': [ 'error', { 'ts-ignore': 'allow-with-description' }, ], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/type-annotation-spacing': ['error'], + 'linebreak-style': 'off', + 'class-methods-use-this': 'off', + 'import/no-unresolved': 'off', '@typescript-eslint/no-this-alias': [ 'error', { @@ -38,33 +48,27 @@ module.exports = { allowedNames: ['self'], }, ], - '@typescript-eslint/type-annotation-spacing': ['error'], - 'class-methods-use-this': 'off', + 'require-yield': 'off', + 'no-plusplus': 'off', + 'import/no-cycle': 'off', + 'prefer-rest-params': 'off', 'ember/no-mixins': 'off', 'ember/require-computed-property-dependencies': 'off', - 'func-names': ['error', 'always', { generators: 'never' }], - 'import/no-cycle': 'off', - 'import/no-unresolved': 'off', - 'linebreak-style': 'off', - 'no-console': ['error', { allow: ['warn', 'error'] }], 'no-param-reassign': ['error', { props: false }], - 'no-plusplus': 'off', - 'prefer-rest-params': 'off', - 'prettier/prettier': 'error', - 'require-yield': 'off', + 'func-names': ['error', 'always', { generators: 'never' }], }, overrides: [ // node files { files: [ - 'config/**/*.js', 'ember-cli-build.js', - 'lib/*/index.js', 'testem.js', + 'config/**/*.js', + 'lib/*/index.js', ], parserOptions: { - ecmaVersion: 2015, sourceType: 'script', + ecmaVersion: 2015, }, env: { browser: false, 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..d1206bbf7 --- /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 { TinySnapshot } 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/api-token-selection.hbs b/app/components/api-token-selection.hbs new file mode 100644 index 000000000..bf960922b --- /dev/null +++ b/app/components/api-token-selection.hbs @@ -0,0 +1,133 @@ +
+
API-Tokens
+
+ + + + + + + + + + + + {{#each + (sort-by + (concat this.sortProperty ':' this.sortOrder) 'createdAt' @apiTokens + ) + as |apiToken| + }} + + + + + + + + {{else}} + There are no saved API-Tokens. + {{/each}} + + + + +
NameAPI TokenCreatedExpires
{{apiToken.name}} {{apiToken.token}} {{this.formatDate apiToken.createdAt true}}{{this.formatDate apiToken.expires}} + +
+
+ + {{svg-jar 'plus-16' class='octicon'}} + +
+
+
+
+
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + Cancel + Save + + +
+
\ 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..799a1762e --- /dev/null +++ b/app/components/api-token-selection.ts @@ -0,0 +1,141 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import UserApiTokenService, { + ApiToken, +} from 'explorviz-frontend/services/user-api-token'; +import { format } from 'date-fns'; +import convertDate from 'explorviz-frontend/utils/helpers/time-convter'; + +export default class ApiTokenSelectionComponent extends Component { + today: string = format(new Date().getTime() + 86400 * 1000, 'yyyy-MM-dd'); + + @service('user-api-token') + userApiTokenService!: UserApiTokenService; + + @tracked + sortProperty: keyof ApiToken = 'createdAt'; + + @tracked + sortOrder: 'asc' | 'desc' = 'desc'; + + @tracked + createToken: boolean = false; + + @tracked + name: string = ''; + + @tracked + expDate: number | null = null; + + @tracked + token: string = ''; + + @tracked + hostUrl: string = ''; + + @tracked + saveBtnDisabled: boolean = true; + + @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; + } + } + + @action + async deleteApiToken(apiToken: ApiToken) { + await this.userApiTokenService.deleteApiToken(apiToken.token, apiToken.uid); + if (localStorage.getItem('gitAPIToken') !== null) { + if (localStorage.getItem('gitAPIToken') === JSON.stringify(apiToken)) { + localStorage.removeItem('gitAPIToken'); + localStorage.removeItem('gitProject'); + } + } + } + + @action + openMenu() { + this.createToken = true; + } + + @action + closeMenu() { + this.reset(); + this.createToken = false; + } + + @action + async createApiToken() { + this.userApiTokenService.createApiToken( + this.name, + this.token, + this.hostUrl, + this.expDate + ); + this.reset(); + } + + @action + reset() { + this.name = ''; + this.expDate = null; + this.token = ''; + this.createToken = false; + this.hostUrl = ''; + } + + @action + updateName(event: InputEvent) { + const target: HTMLInputElement = event.target as HTMLInputElement; + this.name = target.value; + this.canSaveToken(); + } + + @action + updateToken(event: InputEvent) { + const target: HTMLInputElement = event.target as HTMLInputElement; + this.token = target.value; + this.canSaveToken(); + } + + @action + updateHostUrl(event: InputEvent) { + const target: HTMLInputElement = event.target as HTMLInputElement; + this.hostUrl = target.value; + this.canSaveToken(); + } + + @action + updateExpDate(event: InputEvent) { + const target: HTMLInputElement = event.target as HTMLInputElement; + const date = convertDate(target.value); + this.expDate = date; + } + + @action + canSaveToken() { + if (this.token !== '' && this.name !== '' && this.hostUrl !== '') { + this.saveBtnDisabled = false; + } else { + this.saveBtnDisabled = true; + } + } + + formatDate(date: number, showMin: boolean): string { + if (date === 0) { + return '-'; + } else if (showMin) { + return format(new Date(date), 'dd/MM/yyyy, HH:mm'); + } else return format(new Date(date), 'dd/MM/yyyy'); + } +} diff --git a/app/components/delete-snapshot.hbs b/app/components/delete-snapshot.hbs new file mode 100644 index 000000000..22875ea5f --- /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..261d94e87 --- /dev/null +++ b/app/components/delete-snapshot.ts @@ -0,0 +1,20 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import SnapshotTokenService, { + TinySnapshot, +} from 'explorviz-frontend/services/snapshot-token'; + +export default class DeleteSnapshotComponent extends Component { + @service('snapshot-token') + snapshotService!: SnapshotTokenService; + + @action + async deleteSnapshot( + snapShot: TinySnapshot, + isShared: boolean, + subscribed: boolean + ) { + this.snapshotService.deleteSnapshot(snapShot, isShared, subscribed); + } +} diff --git a/app/components/page-setup/navbar.hbs b/app/components/page-setup/navbar.hbs index a1566d0e6..394b97654 100644 --- a/app/components/page-setup/navbar.hbs +++ b/app/components/page-setup/navbar.hbs @@ -76,6 +76,16 @@ {{/if}} +
  • + +
  • +
        {{/each}}
      +
        + + Upload to Gitlab + +
          {{/each}} - -
          -
          +
          +
          Create Issue - - {{#if this.issues.length}} - - Upload to Gitlab - - {{/if}}
            {{/if}} +
            +
            + + + + + + +
            + +
            + +
            + +
            +
            +
            + +
            +
            + +
            +
            +
            + + Cancel + Save + +
            \ No newline at end of file diff --git a/app/components/visualization/page-setup/sidebar/customizationbar/restructure/restructure.ts b/app/components/visualization/page-setup/sidebar/customizationbar/restructure/restructure.ts index b83665184..351685fff 100644 --- a/app/components/visualization/page-setup/sidebar/customizationbar/restructure/restructure.ts +++ b/app/components/visualization/page-setup/sidebar/customizationbar/restructure/restructure.ts @@ -10,6 +10,20 @@ import { LandscapeData } from 'explorviz-frontend/utils/landscape-schemes/landsc import { DynamicLandscapeData } from 'explorviz-frontend/utils/landscape-schemes/dynamic/dynamic-data'; import CollaborationSession from 'collaboration/services/collaboration-session'; import Changelog from 'explorviz-frontend/services/changelog'; +import { format } from 'date-fns'; +import convertDate from 'explorviz-frontend/utils/helpers/time-convter'; +import PopupData from 'explorviz-frontend/components/visualization/rendering/popups/popup-data'; +import { LandscapeToken } from 'explorviz-frontend/services/landscape-token'; +import AnnotationData from 'explorviz-frontend/components/visualization/rendering/annotations/annotation-data'; +import SnapshotTokenService, { + SnapshotToken, +} from 'explorviz-frontend/services/snapshot-token'; +import RoomSerializer from 'collaboration/services/room-serializer'; +import TimestampRepository from 'explorviz-frontend/services/repos/timestamp-repository'; +import LocalUser from 'collaboration/services/local-user'; +import Auth from 'explorviz-frontend/services/auth'; +import ENV from 'explorviz-frontend/config/environment'; +import { ApiToken } from 'explorviz-frontend/services/user-api-token'; interface VisualizationPageSetupSidebarRestructureArgs { landscapeData: LandscapeData; @@ -18,11 +32,23 @@ interface VisualizationPageSetupSidebarRestructureArgs { dynamicData: DynamicLandscapeData ) => void; visualizationPaused: boolean; + popUpData: PopupData[]; + landscapeToken: LandscapeToken; + annotationData: AnnotationData[]; + minimizedAnnotations: AnnotationData[]; + userApiToknes: ApiToken; toggleVisualizationUpdating: () => void; removeTimestampListener: () => void; } +const { shareSnapshot, gitlabApi } = ENV.backendAddresses; + export default class VisualizationPageSetupSidebarRestructure extends Component { + today: string = format(new Date().getTime() + 86400 * 1000, 'yyyy-MM-dd'); + + @service('auth') + auth!: Auth; + @service('repos/application-repository') applicationRepo!: ApplicationRepository; @@ -35,11 +61,29 @@ export default class VisualizationPageSetupSidebarRestructure extends Component< @service('changelog') changeLog!: Changelog; + @service('snapshot-token') + snapshotService!: SnapshotTokenService; + + @service('room-serializer') + roomSerializer!: RoomSerializer; + + @service('repos/timestamp-repository') + timestampRepo!: TimestampRepository; + + @service('local-user') + localUser!: LocalUser; + @service('collaboration-session') private collaborationSession!: CollaborationSession; + // @tracked + // token: string = localStorage.getItem('gitAPIToken') || ''; + @tracked - token: string = localStorage.getItem('gitAPIToken') || ''; + token: ApiToken | null = + localStorage.getItem('gitAPIToken') !== null + ? JSON.parse(localStorage.getItem('gitAPIToken')!) + : null; @tracked issueURL: string = localStorage.getItem('gitIssue') || ''; @@ -68,15 +112,46 @@ export default class VisualizationPageSetupSidebarRestructure extends Component< @tracked restructureMode: boolean = this.landscapeRestructure.restructureMode; - @tracked - saveCredBtnDisabled: boolean = true; - @tracked createAppBtnDisabled: boolean = true; @tracked uploadIssueBtnDisabled: boolean = false; + @tracked + snapshotModal: boolean = false; + + @tracked + index: number | null = null; + + @tracked + snapshotName: string | null = null; + + @tracked + saveSnaphotBtnDisabled: boolean = true; + + @tracked + expDate: number | null = null; + + @tracked + createPersonalSnapshot = false; + + @tracked + disabledSelectProject: boolean = this.token === null ? true : false; + + @tracked + gitLabProjects: any = []; + + @tracked + project: { id: string; name: string } | undefined = + localStorage.getItem('gitProject') !== null + ? JSON.parse(localStorage.getItem('gitProject')!) + : undefined; + + @tracked + saveCredBtnDisabled: boolean = + this.token !== null && this.project !== undefined ? false : true; + get clip_board() { return this.landscapeRestructure.clipboard; } @@ -226,12 +301,60 @@ export default class VisualizationPageSetupSidebarRestructure extends Component< } @action - updateToken(event: InputEvent) { - const target = event.target as HTMLInputElement; - this.token = target.value; + updateToken(token: ApiToken) { + if (JSON.stringify(token) !== JSON.stringify(this.token)) { + this.project = undefined; + } + this.token = token; + this.disabledSelectProject = false; + this.canSaveCredentials(); + } + + @action + onSelect(project: any) { + this.project = project as { id: string; name: string }; this.canSaveCredentials(); } + @action + loadProjects() { + const token = this.token!.token; + const hostUrl = this.token!.hostUrl; + const body = { api_token: token, host_url: hostUrl }; + this.gitLabProjects = new Promise<{ id: string; name: string }[]>( + (resolve) => { + // fetch(`${gitlabApi}/get_all_projects/${token}/${hostUrl}`) + fetch(`${gitlabApi}/get_all_projects`, { + method: 'POST', + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/json' }, + }) + .then(async (response: Response) => { + console.log(response); + if (response.ok) { + const projects = (await response.json()) as { + id: string; + name: string; + }[]; + resolve(projects); + } else { + this.toastHandlerService.showErrorToastMessage( + 'Could not load projects.' + ); + resolve([]); + } + }) + .catch(async (e) => { + console.log(e); + resolve([]); + this.toastHandlerService.showErrorToastMessage( + 'Network error: Could not load projects.' + ); + }); + } + ); + } + @action updateIssueURL(event: InputEvent) { const target = event.target as HTMLInputElement; @@ -258,7 +381,8 @@ export default class VisualizationPageSetupSidebarRestructure extends Component< @action canSaveCredentials() { - this.saveCredBtnDisabled = this.token === '' || this.issueURL === ''; + this.saveCredBtnDisabled = + this.token === null || this.project === undefined; if (this.uploadURL) this.canUpload(); } @@ -305,6 +429,25 @@ export default class VisualizationPageSetupSidebarRestructure extends Component< this.issues[index].content = target.value; } + @action + addSnapshotLink(index: number, url: string, name: string) { + const updatedIssue = { + ...this.issues[index], + content: this.issues[index].content + '\n' + name + ': ' + url, + }; + + const updatedIssues = []; + for (const [issueIndex, issue] of this.issues.entries()) { + if (index === issueIndex) { + updatedIssues.push(updatedIssue); + } else { + updatedIssues.push(issue); + } + } + + this.issues = updatedIssues; + } + @action deleteIssue(index: number) { this.issues.removeAt(index); @@ -324,6 +467,105 @@ export default class VisualizationPageSetupSidebarRestructure extends Component< this.canUpload(); } + @action + openSnapshotModal(index: number) { + this.snapshotModal = true; + this.index = index; + } + + @action + closeSnaphshotModal() { + this.snapshotModal = false; + this.index = null; + this.snapshotName = null; + this.expDate = null; + this.saveSnaphotBtnDisabled = true; + this.createPersonalSnapshot = false; + } + + @action + updateName(event: InputEvent) { + const target: HTMLInputElement = event.target as HTMLInputElement; + this.snapshotName = target.value; + this.canSaveSnapShot(); + } + + @action + canSaveSnapShot() { + if (this.snapshotName !== '') { + this.saveSnaphotBtnDisabled = false; + } else { + this.saveSnaphotBtnDisabled = true; + } + } + + @action + updateExpDate(event: InputEvent) { + const target: HTMLInputElement = event.target as HTMLInputElement; + const date = convertDate(target.value); + this.expDate = date; + } + + @action + updatePersonalSnapshot() { + this.createPersonalSnapshot = !this.createPersonalSnapshot; + } + + @action + createSnapshot() { + const allAnnotations = this.args.annotationData.concat( + this.args.minimizedAnnotations + ); + + const createdAt: number = new Date().getTime(); + const saveRoom = this.roomSerializer.serializeRoom( + this.args.popUpData, + allAnnotations, + true + ); + + const timestamps = this.timestampRepo.getTimestamps( + this.args.landscapeToken.value + ); + + const sharedToken: SnapshotToken = { + owner: this.auth.user!.sub, + createdAt: createdAt, + name: this.snapshotName!, + landscapeToken: this.args.landscapeToken, + structureData: { + structureLandscapeData: this.args.landscapeData.structureLandscapeData, + dynamicLandscapeData: this.args.landscapeData.dynamicLandscapeData, + }, + serializedRoom: saveRoom, + timestamps: { timestamps: timestamps }, + camera: { + x: this.localUser.camera.position.x, + y: this.localUser.camera.position.y, + z: this.localUser.camera.position.z, + }, + isShared: true, + subscribedUsers: { subscriberList: [] }, + deleteAt: this.expDate !== null ? this.expDate : 0, + }; + + if (this.createPersonalSnapshot) { + const personalToken: SnapshotToken = { + ...sharedToken, + isShared: false, + }; + this.snapshotService.saveSnapshot(personalToken); + } + + this.snapshotService.saveSnapshot(sharedToken); + + const snapshotURL = `${shareSnapshot}visualization?landscapeToken=${sharedToken.landscapeToken.value}&owner=${sharedToken.owner}&createdAt=${sharedToken.createdAt}&sharedSnapshot=${true}`; + + this.addSnapshotLink(this.index!, snapshotURL, sharedToken.name); + + this.closeSnaphshotModal(); + } + @action deleteEntry(index: number) { const entry = this.changeLog.changeLogEntries[index]; @@ -340,9 +582,12 @@ export default class VisualizationPageSetupSidebarRestructure extends Component< @action saveGitlabCredentials() { - localStorage.setItem('gitAPIToken', this.token); - localStorage.setItem('gitIssue', this.issueURL); - localStorage.setItem('gitUpload', this.uploadURL); + localStorage.setItem('gitAPIToken', JSON.stringify(this.token!)); + localStorage.setItem('gitProject', JSON.stringify(this.project!)); + // localStorage.setItem('gitUpload', this.uploadURL); + this.toastHandlerService.showSuccessToastMessage( + 'Git credentials successfully saved.' + ); } @action @@ -358,75 +603,74 @@ export default class VisualizationPageSetupSidebarRestructure extends Component< } @action - async uploadIssueToGitLab() { - try { - const uploadPromises = this.issues.map(async (issue) => { - // Upload the screenshots and get their URLs - const screenshotUrls = await Promise.all( - issue.screenshots.map((screenshot) => - this.uploadImageToRepository(screenshot) - ) - ); + async uploadIssueToGitLab(index: number) { + this.uploadImageToRepository(this.issues[index].screenshots[0]); - // Append the screenshot URLs to the issue content - const contentWithScreenshots = `${issue.content}\n${screenshotUrls - .map((url) => `![Screenshot](${url})`) - .join('\n')}`; + const screenshotUrls = await Promise.all( + this.issues[index].screenshots.map((screenshot) => + this.uploadImageToRepository(screenshot) + ) + ); - // Upload the issue - const response = await fetch(this.issueURL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.token}`, - }, - body: JSON.stringify({ - title: issue.title, - description: contentWithScreenshots, - }), - }); - - if (!response.ok) { + const contentWithScreenShots = `${this.issues[index].content}\n${screenshotUrls + .map((url) => `![Screenshot](${url})`) + .join('\n')}`; + + const body = { + project_id: this.project!.id, + api_token: this.token!.token, + host_url: this.token!.hostUrl, + title: this.issues[index].title, + description: contentWithScreenShots, + }; + + fetch(`${gitlabApi}/create_issue`, { + method: 'POST', + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/json; charset=UTF-8' }, + }) + .then(async (response: Response) => { + if (response.ok) { + this.toastHandlerService.showSuccessToastMessage( + 'Successfully created Issue.' + ); + } else { this.toastHandlerService.showErrorToastMessage( - `Failed to upload issue: ${issue.title}` + 'Could not load projects.' ); - throw new Error(`Failed to upload issue: ${issue.title}`); } - - return response.json(); + }) + .catch(async () => { + this.toastHandlerService.showErrorToastMessage( + 'Network error: Could not load projects.' + ); }); - - const results = await Promise.all(uploadPromises); - - this.toastHandlerService.showSuccessToastMessage( - 'Issue(s) successfully uploaded' - ); - return results; - } catch (error) { - console.error(error); - return []; - } + this.deleteIssue(index); } async uploadImageToRepository(dataURL: string) { const blob = await fetch(dataURL).then((res) => res.blob()); - const imgFile = new File([blob], 'screenshotCanva.png', { + const imgFile = new File([blob], 'screenshotCanvas.png', { type: 'image/png', }); const formData = new FormData(); formData.append('file', imgFile); - const res = await fetch(this.uploadURL, { - method: 'POST', - headers: { - Authorization: `Bearer ${this.token}`, - }, - body: formData, - }); + const res = await fetch( + // `https://${this.token!.hostUrl}/api/v4/projects/${this.project!.id}/uploads`, + `${this.token!.hostUrl}/api/v4/projects/${this.project!.id}/uploads`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${this.token!.token}`, + }, + body: formData, + } + ); if (!res.ok) { - throw new Error('Failed to Upload Image'); + this.toastHandlerService.showErrorToastMessage('Could not upload Image.'); } const jsonRes = await res.json(); diff --git a/app/components/visualization/page-setup/sidebar/customizationbar/settings/settings.ts b/app/components/visualization/page-setup/sidebar/customizationbar/settings/settings.ts index ecb321b2b..9105d75a6 100644 --- a/app/components/visualization/page-setup/sidebar/customizationbar/settings/settings.ts +++ b/app/components/visualization/page-setup/sidebar/customizationbar/settings/settings.ts @@ -74,6 +74,7 @@ export default class Settings extends Component { Highlighting: [], Effects: [], Popups: [], + Annotations: [], 'Virtual Reality': [], Debugging: [], }; @@ -204,6 +205,7 @@ export default class Settings extends Component { this.args.setGamepadSupport(value); break; default: + break; } } diff --git a/app/components/visualization/page-setup/sidebar/customizationbar/snapshot/snapshot-opener.hbs b/app/components/visualization/page-setup/sidebar/customizationbar/snapshot/snapshot-opener.hbs new file mode 100644 index 000000000..83889223f --- /dev/null +++ b/app/components/visualization/page-setup/sidebar/customizationbar/snapshot/snapshot-opener.hbs @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/app/components/visualization/page-setup/sidebar/customizationbar/snapshot/snapshot.hbs b/app/components/visualization/page-setup/sidebar/customizationbar/snapshot/snapshot.hbs new file mode 100644 index 000000000..2e41cf6c6 --- /dev/null +++ b/app/components/visualization/page-setup/sidebar/customizationbar/snapshot/snapshot.hbs @@ -0,0 +1,30 @@ +
            +
            Snapshot Name:
            + +
            +
            + + Create Snapshot + + + Export Snapshot + +
            \ No newline at end of file diff --git a/app/components/visualization/page-setup/sidebar/customizationbar/snapshot/snapshot.ts b/app/components/visualization/page-setup/sidebar/customizationbar/snapshot/snapshot.ts new file mode 100644 index 000000000..739697d72 --- /dev/null +++ b/app/components/visualization/page-setup/sidebar/customizationbar/snapshot/snapshot.ts @@ -0,0 +1,152 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import RoomSerializer from 'collaboration/services/room-serializer'; +import PopupData from 'explorviz-frontend/components/visualization/rendering/popups/popup-data'; +import Auth from 'explorviz-frontend/services/auth'; +import SnapshotTokenService, { + SnapshotToken, +} from 'explorviz-frontend/services/snapshot-token'; +import ToastHandlerService from 'explorviz-frontend/services/toast-handler'; +import { LandscapeToken } from 'explorviz-frontend/services/landscape-token'; +import AnnotationData from 'explorviz-frontend/components/visualization/rendering/annotations/annotation-data'; +import LocalUser from 'collaboration/services/local-user'; +import TimestampRepository from 'explorviz-frontend/services/repos/timestamp-repository'; +import { LandscapeData } from 'explorviz-frontend/utils/landscape-schemes/landscape-data'; + +interface Args { + landscapeData: LandscapeData; + popUpData: PopupData[]; + landscapeToken: LandscapeToken; + annotationData: AnnotationData[]; + minimizedAnnotations: AnnotationData[]; +} + +export default class VisualizationPageSetupSidebarCustomizationbarSnapshotSnapshotComponent extends Component { + @service('auth') + auth!: Auth; + + @service('room-serializer') + roomSerializer!: RoomSerializer; + + @service('snapshot-token') + snapshotService!: SnapshotTokenService; + + @service('toast-handler') + toastHandler!: ToastHandlerService; + + @service('local-user') + localUser!: LocalUser; + + @service('repos/timestamp-repository') + timestampRepo!: TimestampRepository; + + @tracked + saveSnaphotBtnDisabled: boolean = true; + + @tracked + snapshotName: string = ''; + + @action + canSaveSnapShot() { + if (this.snapshotName !== '') { + this.saveSnaphotBtnDisabled = false; + } else { + this.saveSnaphotBtnDisabled = true; + } + } + + @action + updateName(event: InputEvent) { + const target: HTMLInputElement = event.target as HTMLInputElement; + this.snapshotName = target.value; + this.canSaveSnapShot(); + } + + @action + async saveSnapshot() { + const allAnnotations = this.args.annotationData.concat( + this.args.minimizedAnnotations + ); + + const createdAt: number = new Date().getTime(); + const saveRoom = this.roomSerializer.serializeRoom( + this.args.popUpData, + allAnnotations, + true + ); + + const timestamps = + this.timestampRepo.getTimestampsForCommitId('cross-commit'); + + const content: SnapshotToken = { + owner: this.auth.user!.sub, + createdAt: createdAt, + name: this.snapshotName, + landscapeToken: this.args.landscapeToken, + structureData: { + structureLandscapeData: this.args.landscapeData.structureLandscapeData, + dynamicLandscapeData: this.args.landscapeData.dynamicLandscapeData, + }, + serializedRoom: saveRoom, + timestamps: { timestamps: timestamps }, + camera: { + x: this.localUser.camera.position.x, + y: this.localUser.camera.position.y, + z: this.localUser.camera.position.z, + }, + isShared: false, + subscribedUsers: { subscriberList: [] }, + deleteAt: 0, + }; + + this.snapshotService.saveSnapshot(content); + this.reset(); + } + + @action + exportSnapshot() { + const allAnnotations = this.args.annotationData.concat( + this.args.minimizedAnnotations + ); + + const createdAt: number = new Date().getTime(); + const saveRoom = this.roomSerializer.serializeRoom( + this.args.popUpData, + allAnnotations, + true + ); + + const timestamps = + this.timestampRepo.getTimestampsForCommitId('cross-commit'); + + const content: SnapshotToken = { + owner: this.auth.user!.sub, + createdAt: createdAt, + name: this.snapshotName, + landscapeToken: this.args.landscapeToken, + structureData: { + structureLandscapeData: this.args.landscapeData.structureLandscapeData, + dynamicLandscapeData: this.args.landscapeData.dynamicLandscapeData, + }, + serializedRoom: saveRoom, + timestamps: { timestamps: timestamps }, + camera: { + x: this.localUser.camera.position.x, + y: this.localUser.camera.position.y, + z: this.localUser.camera.position.z, + }, + isShared: false, + subscribedUsers: { subscriberList: [] }, + deleteAt: 0, + }; + this.snapshotService.exportFile(content); + this.reset(); + } + + reset() { + this.snapshotName = ''; + this.saveSnaphotBtnDisabled = true; + } +} diff --git a/app/components/visualization/rendering/annotations/annotation-coordinator.hbs b/app/components/visualization/rendering/annotations/annotation-coordinator.hbs new file mode 100644 index 000000000..7a860354f --- /dev/null +++ b/app/components/visualization/rendering/annotations/annotation-coordinator.hbs @@ -0,0 +1,252 @@ +{{! template-lint-disable no-pointer-down-event-binding }} +
            + {{#if @isMovable}} + {{#if @annotationData.isAssociated}} + {{#if @annotationData.wasMoved}} +
            + +
            + +
            +
            +
            +
            + +
            +
            + {{#if @annotationData.inEdit}} + + {{else}} + + {{/if}} + + + {{svg-jar 'location-16' class='octicon align-middle'}} + + Ping + + + + {{#if (not this.collaborationSession.isOnline)}} + + {{svg-jar 'share-android-16' class='octicon align-right'}} + + This is not an online session. + + + {{else if @annotationData.shared}} + + {{svg-jar 'share-android-16' class='octicon align-right'}} + + Annotation is shared + + + {{else if this.collaborationSession.isOnline}} + + {{svg-jar 'share-android-16' class='octicon align-middle'}} + + Share annotation with other users. + + + {{/if}} + + + _ + + + + {{svg-jar 'trash-16' class='octicon align-right'}} + +
            + {{#unless @annotationData.hidden}} + {{#if @annotationData.inEdit}} +
            +