Skip to content

Commit

Permalink
[#1894] Add embeddable ramp widget (#1988)
Browse files Browse the repository at this point in the history
The ability to copy a link for an embeddable ramp widget with the click 
of a button and embed the ramp widget into any existing website will be 
a useful feature. This allows users who intend to feature any 
contributions made to a repository/projects in their portfolio/any 
website, especially as I feel it is both eye-catching and provides a 
lot of useful information. The embedded ramps should follow any filters 
configured by the users above so that the information displayed by the 
ramps can be customized. 

Let's add a button to allow users to copy and embed widgets.
  • Loading branch information
MarcusTXK authored Apr 30, 2023
1 parent e7b139c commit 1cc3e67
Show file tree
Hide file tree
Showing 13 changed files with 809 additions and 473 deletions.
20 changes: 18 additions & 2 deletions docs/dg/report.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ The tabbed interface is responsible for loading various modules such as authorsh
## Javascript and Vue files

- **main.js** - sets up plugins and 3rd party components used in the report
- [**app.vue**](#app-app-vue) - module that supports the report interface
- [**app.vue**](#app-app-vue) - module that renders the `router-view`
- [**api.js**](#data-loader-api-js) - loading and parsing of the report content
- [**c_home.vue**](#home-view-c-home-vue) - module that supports the report interface
- [**c_widget.vue**](#widget-view-c-widget-vue) - module that supports the widget interface
- [**c_summary.vue**](#summary-view-c-summary-vue) - module that supports the summary view
- [**c_authorship.vue**](#authorship-view-c-authorship-vue) - module that supports the authorship tab view
- [**c_zoom.vue**](#zoom-view-c-zoom-vue) - module that supports the zoom tab view
Expand All @@ -58,7 +60,7 @@ This contains the logic for the main VueJS object, `app.vue`, which is the entry

Vuex in `store.js` is used to pass the necessary data into the relevant modules.

`c_summary`, `c_authorship`, `c_zoom`, `c_segment`, and `c_ramp` are components embedded into the report and will render the corresponding content based on the data passed into it from Vuex.
`c_home`, `c_widget`, `c_summary`, `c_authorship`, `c_zoom`, `c_segment`, and `c_ramp` are components embedded into the report and will render the corresponding content based on the data passed into it from Vuex.

### Loading of report information
The main Vue object depends on the `summary.json` data to determine the right `commits.json` files to load into memory. This is handled by `api.js`, which loads the relevant file information from the network files if available; otherwise, a report archive, `archive.zip`, has to be used.
Expand Down Expand Up @@ -86,6 +88,20 @@ After the JSON files are loaded from their respective sources, the data will be

For the basic skeleton of `window.REPOS`, refer to the generated `summary.json` file in the report for more details.

<!-- ==================================================================================================== -->

## Home view ([c-home.vue](https://github.com/reposense/RepoSense/blob/master/frontend/src/views/c-home.vue))

The `c_home` module is in charge of rendering the main report, and renders `c_resizer`, `c_summary`, `c_authorship` and `c_zoom`.

<!-- ==================================================================================================== -->

## Widget view ([c-widget.vue](https://github.com/reposense/RepoSense/blob/master/frontend/src/views/c-widget.vue))

The `c_widget` module is in charge of rendering the widget from the `iframe` and only renders `c_summary`. An additional prop, `isWidgetMode`, is passed to `c_summary` so it knows to render as a widget and omit unnecessary elements.



<!-- ==================================================================================================== -->

## Summary view ([c-summary.vue](https://github.com/reposense/RepoSense/blob/master/frontend/src/views/c-summary.vue))
Expand Down
17 changes: 15 additions & 2 deletions docs/ug/sharingReports.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% set title = "Sharing reports" %}
<frontmatter>
title: "{{ title | safe }}"
pageNav: 3
title: "{{ title | safe }}"
pageNav: 3
</frontmatter>

{% from 'scripts/macros.njk' import embed with context %}
Expand All @@ -11,6 +11,7 @@
<div class="lead">

**Often, you would want to share the RepoSense report with others.** For example, a teacher using RepoSense for a programming class might want to share the report privately with tutors or publish it so that everyone can see it.

</div>

The sections below explain various ways of sharing a RepoSense report.
Expand All @@ -35,4 +36,16 @@ As RepoSense reports are in a web page format, you can publish a report by simpl

{{ embed("Appendix: **Using RepoSense with Netlify**", "withNetlify.md") }}

<!-- ------------------------------------------------------------------------------------------------------ -->

### Embeddable Widgets

Published reports can additionally be embedded in other websites through `iframes`. Simply click the clipboard icon to generate and copy the iframe for your desired section of the report for either a single ramp chart or a group of ramp charts. Paste the iframe in any HTML supported document to render it.

A sample iframe would look like:

```html
<iframe src="XXX" frameborder="0" width="800px" height="XXXpx"></iframe>
```

Adjust the width and height to the desired dimensions as required.
2 changes: 2 additions & 0 deletions frontend/cypress/tests/zoomView/zoomView_switchZoom.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ describe('switch zoom', () => {
.last()
.click();

cy.get('#tabs-wrapper').scrollTo('top');

// check default controls
cy.get('#tab-zoom > .sorting > .sort-by > select:visible')
.should('not.have.value', 'linesOfCode')
Expand Down
229 changes: 228 additions & 1 deletion frontend/src/app.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,240 @@
<template lang="pug">
#app
router-view
loading-overlay.overlay-loader(
v-bind:active='loadingOverlayCount > 0',
v-bind:opacity='loadingOverlayOpacity'
)
template(v-slot:default)
i.overlay-loading-icon.fa.fa-spinner.fa-spin()
template(v-slot:after)
h3 {{ loadingOverlayMessage }}

router-view(
v-bind:update-report-zip="updateReportZip",
v-bind:repos="repos",
v-bind:users="users",
v-bind:user-updated="userUpdated",
v-bind:loading-overlay-opacity="loadingOverlayOpacity",
v-bind:tab-type="tabType",
v-bind:creation-date="creationDate",
v-bind:report-generation-time="reportGenerationTime",
v-bind:error-messages="errorMessages"
)
</template>

<script lang='ts'>
import { defineComponent } from 'vue';
import JSZip from 'jszip';
import LoadingOverlay from 'vue-loading-overlay';
import { mapState } from 'vuex';
import { Repo } from './types/types';
import { ErrorMessage } from './types/zod/summary-type';
import { ZoomInfo, AuthorshipInfo } from './types/vuex.d';
const loadingResourcesMessage = 'Loading resources...';
const app = defineComponent({
name: 'app',
components: {
LoadingOverlay,
},
data() {
return {
repos: {} as { [key: string]: Repo },
users: [] as Repo[],
userUpdated: false,
loadingOverlayOpacity: 1,
tabType: 'empty',
creationDate: '',
reportGenerationTime: '',
errorMessages: {} as { [key: string]: ErrorMessage },
};
},
computed: {
...mapState(['loadingOverlayCount', 'loadingOverlayMessage', 'isTabActive']),
},
watch: {
'$store.state.tabZoomInfo': function () {
if (this.$store.state.tabZoomInfo.isRefreshing) {
return;
}
this.activateTab('zoom');
},
'$store.state.tabAuthorshipInfo': function () {
this.activateTab('authorship');
},
},
created() {
try {
window.decodeHash();
} catch (error) {
this.userUpdated = false;
}
this.updateReportDir();
},
methods: {
// model functions //
updateReportZip(evt: Event) {
this.users = [];
const target = evt.target as HTMLInputElement;
if (target.files === null) {
return;
}
JSZip.loadAsync(target.files[0])
.then((zip) => {
window.REPORT_ZIP = zip;
}, () => {
window.alert('Either the .zip file is corrupted, or you uploaded a .zip file that is not generated '
+ 'by RepoSense.');
})
.then(() => this.updateReportView());
},
updateReportDir() {
window.REPORT_ZIP = null;
this.users = [];
this.updateReportView();
},
async updateReportView() {
this.$store.commit('updateLoadingOverlayMessage', loadingResourcesMessage);
this.userUpdated = false;
await this.$store.dispatch('incrementLoadingOverlayCountForceReload', 1);
try {
const summary = await window.api.loadSummary();
if (summary === null) {
return;
}
const {
creationDate,
reportGenerationTime,
errorMessages,
names,
} = summary;
this.creationDate = creationDate;
this.reportGenerationTime = reportGenerationTime;
this.errorMessages = errorMessages;
this.repos = window.REPOS;
await Promise.all(names.map((name) => (
window.api.loadCommits(name)
)));
this.loadingOverlayOpacity = 0.5;
this.getUsers();
this.renderTabHash();
this.userUpdated = true;
} catch (error) {
window.alert(error);
} finally {
this.$store.commit('incrementLoadingOverlayCount', -1);
}
},
getUsers() {
const full: Repo[] = [];
Object.keys(this.repos).forEach((repo) => {
if (this.repos[repo].users) {
full.push(this.repos[repo]);
}
});
this.users = full;
},
// handle opening of sidebar //
activateTab(tabName: string) {
if (this.$refs.tabWrapper) {
(this.$refs.tabWrapper as HTMLElement).scrollTop = 0;
}
this.tabType = tabName;
this.$store.commit('updateTabState', true);
window.addHash('tabType', this.tabType);
window.encodeHash();
},
renderAuthorShipTabHash(minDate: string, maxDate: string) {
const hash = window.hashParams;
const info: AuthorshipInfo = {
author: hash.tabAuthor,
repo: hash.tabRepo,
isMergeGroup: hash.authorshipIsMergeGroup === 'true',
isRefresh: true,
minDate,
maxDate,
location: this.getRepoLink(),
files: [],
};
const tabInfoLength = Object.values(info).filter((x) => x !== null).length;
if (Object.keys(info).length === tabInfoLength) {
this.$store.commit('updateTabAuthorshipInfo', info);
} else if (hash.tabOpen === 'false' || tabInfoLength > 2) {
this.$store.commit('updateTabState', false);
}
},
renderZoomTabHash() {
const hash = window.hashParams;
const zoomInfo: ZoomInfo = {
isRefreshing: true,
zAuthor: hash.zA,
zRepo: hash.zR,
zAvgCommitSize: hash.zACS,
zSince: hash.zS,
zUntil: hash.zU,
zFilterGroup: hash.zFGS,
zFilterSearch: hash.zFS,
zTimeFrame: hash.zFTF,
zIsMerged: hash.zMG === 'true',
zFromRamp: hash.zFR === 'true',
};
const tabInfoLength = Object.values(zoomInfo).filter((x) => x !== null).length;
if (Object.keys(zoomInfo).length === tabInfoLength) {
this.$store.commit('updateTabZoomInfo', zoomInfo);
} else if (hash.tabOpen === 'false' || tabInfoLength > 2) {
this.$store.commit('updateTabState', false);
}
},
renderTabHash() {
const hash = window.hashParams;
if (!hash.tabOpen) {
return;
}
this.$store.commit('updateTabState', hash.tabOpen === 'true');
if (this.isTabActive) {
if (hash.tabType === 'authorship') {
let { since, until } = hash;
// get since and until dates from window if not found in hash
since = since || window.sinceDate;
until = until || window.untilDate;
this.renderAuthorShipTabHash(since, until);
} else {
this.renderZoomTabHash();
}
}
},
getRepoSenseHomeLink() {
const version = window.repoSenseVersion;
if (!version) {
return `${window.HOME_PAGE_URL}/RepoSense/`;
}
return `${window.HOME_PAGE_URL}`;
},
getRepoLink() {
const { REPOS, hashParams } = window;
const { location, branch } = REPOS[hashParams.tabRepo];
if (Object.prototype.hasOwnProperty.call(location, 'organization')) {
return window.getBranchLink(hashParams.tabRepo, branch);
}
return REPOS[hashParams.tabRepo].location.location;
},
},
});
export default app;
Expand Down
37 changes: 25 additions & 12 deletions frontend/src/components/c-ramp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,19 @@
)

template(v-else)
a.ramp__slice(
draggable="false",
v-for="(slice, j) in user.commits",
v-bind:title="getContributionMessage(slice)",
v-on:click="openTabZoom(user, slice, $event)",
v-bind:class="`ramp__slice--color${getSliceColor(slice)}`",
v-bind:style="{\
zIndex: user.commits.length - j,\
borderLeftWidth: `${getWidth(slice)}em`,\
right: `${(getSlicePos(tframe === 'day' ? slice.date : slice.endDate) * 100)}%` \
}"
)
a(v-bind:href="getReportLink()", target="_blank")
.ramp__slice(
draggable="false",
v-for="(slice, j) in user.commits",
v-bind:title="getContributionMessage(slice)",
v-on:click="openTabZoom(user, slice, $event)",
v-bind:class="`ramp__slice--color${getSliceColor(slice)}`",
v-bind:style="{\
zIndex: user.commits.length - j,\
borderLeftWidth: `${getWidth(slice)}em`,\
right: `${(getSlicePos(tframe === 'day' ? slice.date : slice.endDate) * 100)}%` \
}"
)
</template>

<script>
Expand Down Expand Up @@ -77,6 +78,10 @@ export default {
type: String,
default: '',
},
isWidgetMode: {
type: Boolean,
default: false,
},
},
data() {
return {
Expand Down Expand Up @@ -188,6 +193,14 @@ export default {
evt.preventDefault();
}
},
getReportLink() {
if (this.isWidgetMode) {
const url = window.location.href;
const regexToRemoveWidget = /([?&])((chartIndex|chartGroupIndex)=\d+)/g;
return url.replace(regexToRemoveWidget, '');
}
return undefined;
},
},
};
</script>
Expand Down
Loading

0 comments on commit 1cc3e67

Please sign in to comment.