From 1cc3e67923ba34a6dc5d01a501d492c27e2374c1 Mon Sep 17 00:00:00 2001
From: Marcus Tang <50147457+MarcusTXK@users.noreply.github.com>
Date: Sun, 30 Apr 2023 10:11:04 +0800
Subject: [PATCH] [#1894] Add embeddable ramp widget (#1988)
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.
---
docs/dg/report.md | 20 +-
docs/ug/sharingReports.md | 17 +-
.../tests/zoomView/zoomView_switchZoom.cy.js | 2 +
frontend/src/app.vue | 229 +++++++++++++++-
frontend/src/components/c-ramp.vue | 37 ++-
frontend/src/components/c-summary-charts.vue | 191 ++++++++++++--
frontend/src/router/index.ts | 7 +
frontend/src/styles/style.scss | 4 +
frontend/src/styles/summary-chart.scss | 228 ++++++++++++++++
frontend/src/utils/load-font-awesome-icons.js | 4 +-
frontend/src/views/c-home.vue | 228 +++-------------
frontend/src/views/c-summary.vue | 249 ++----------------
frontend/src/views/c-widget.vue | 66 +++++
13 files changed, 809 insertions(+), 473 deletions(-)
create mode 100644 frontend/src/styles/summary-chart.scss
create mode 100644 frontend/src/views/c-widget.vue
diff --git a/docs/dg/report.md b/docs/dg/report.md
index 1fa6baad75..6a8d9436c0 100644
--- a/docs/dg/report.md
+++ b/docs/dg/report.md
@@ -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
@@ -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.
@@ -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))
diff --git a/docs/ug/sharingReports.md b/docs/ug/sharingReports.md
index 339567552c..c532e83e10 100644
--- a/docs/ug/sharingReports.md
+++ b/docs/ug/sharingReports.md
@@ -1,7 +1,7 @@
{% set title = "Sharing reports" %}
- title: "{{ title | safe }}"
- pageNav: 3
+title: "{{ title | safe }}"
+pageNav: 3
{% from 'scripts/macros.njk' import embed with context %}
@@ -11,6 +11,7 @@
**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.
+
The sections below explain various ways of sharing a RepoSense report.
@@ -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
+
+```
+Adjust the width and height to the desired dimensions as required.
diff --git a/frontend/cypress/tests/zoomView/zoomView_switchZoom.cy.js b/frontend/cypress/tests/zoomView/zoomView_switchZoom.cy.js
index 415515ca80..00127f933b 100644
--- a/frontend/cypress/tests/zoomView/zoomView_switchZoom.cy.js
+++ b/frontend/cypress/tests/zoomView/zoomView_switchZoom.cy.js
@@ -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')
diff --git a/frontend/src/app.vue b/frontend/src/app.vue
index 25ac6de6f6..b167450c7c 100644
--- a/frontend/src/app.vue
+++ b/frontend/src/app.vue
@@ -1,13 +1,240 @@
#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"
+ )
diff --git a/frontend/src/components/c-summary-charts.vue b/frontend/src/components/c-summary-charts.vue
index 8b7b0cde4f..27c026baaa 100644
--- a/frontend/src/components/c-summary-charts.vue
+++ b/frontend/src/components/c-summary-charts.vue
@@ -1,13 +1,20 @@
#summary-charts
- .summary-charts(v-for="(repo, i) in filteredRepos")
+ .summary-charts(
+ v-for="(repo, i) in filteredRepos",
+ v-bind:ref="'summary-charts-' + i",
+ v-bind:style="isChartGroupWidgetMode ? {'marginBottom': 0} : {}"
+ )
.summary-charts__title(
v-if="filterGroupSelection !== 'groupByNone'",
+ v-bind:ref="'summary-charts-title-' + i",
v-bind:class="{ 'active-background': \
- isSelectedGroup(repo[0].name, repo[0].repoName) }"
+ isSelectedGroup(repo[0].name, repo[0].repoName) && !isChartGroupWidgetMode}"
)
- .summary-charts__title--index {{ i+1 }}
- .summary-charts__title--groupname
+ .summary-charts__title--index(v-if="!isChartGroupWidgetMode") {{ i+1 }}
+ .summary-charts__title--groupname(
+ v-bind:style="isChartGroupWidgetMode ? {'paddingLeft': 0} : {}"
+ )
template(v-if="filterGroupSelection === 'groupByRepos'") {{ repo[0].repoName }}
template(
v-else-if="filterGroupSelection === 'groupByAuthors'",
@@ -17,20 +24,20 @@
.tooltip
| [{{ getGroupTotalContribution(repo) }} lines]
span.tooltip-text(
- v-if="filterGroupSelection === 'groupByRepos'"
+ v-if="filterGroupSelection === 'groupByRepos' && !isChartGroupWidgetMode"
) Total contribution of group
span.tooltip-text(
- v-else-if="filterGroupSelection === 'groupByAuthors'"
+ v-else-if="filterGroupSelection === 'groupByAuthors' && !isChartGroupWidgetMode"
) Total contribution of author
a(
- v-if="!isGroupMerged(getGroupName(repo))",
+ v-if="!isGroupMerged(getGroupName(repo)) && !isChartGroupWidgetMode",
v-on:click="handleMergeGroup(getGroupName(repo))"
)
.tooltip
font-awesome-icon.icon-button(:icon="['fas', 'chevron-up']")
span.tooltip-text Click to merge group
a(
- v-if="isGroupMerged(getGroupName(repo))",
+ v-if="isGroupMerged(getGroupName(repo)) && !isChartGroupWidgetMode",
v-on:click="handleExpandGroup(getGroupName(repo))"
)
.tooltip
@@ -43,7 +50,9 @@
)
.tooltip
font-awesome-icon.icon-button(:icon="getRepoIcon(repo[0])")
- span.tooltip-text {{getGroupRepoLinkMessage(repo[0])}}
+ span.tooltip-text(
+ v-if="!isChartGroupWidgetMode",
+ ) {{getGroupRepoLinkMessage(repo[0])}}
a(
v-else-if="filterGroupSelection === 'groupByAuthors'",
v-bind:class="!isBrokenLink(getAuthorProfileLink(repo[0], repo[0].name)) ? '' : 'broken-link'",
@@ -51,10 +60,12 @@
)
.tooltip
font-awesome-icon.icon-button(icon="user")
- span.tooltip-text {{getAuthorProfileLinkMessage(repo[0])}}
+ span.tooltip-text(
+ v-if="!isChartGroupWidgetMode",
+ ) {{getAuthorProfileLinkMessage(repo[0])}}
template(v-if="isGroupMerged(getGroupName(repo))")
a(
- v-if="filterGroupSelection !== 'groupByAuthors'",
+ v-if="filterGroupSelection !== 'groupByAuthors' && !isChartGroupWidgetMode",
onclick="deactivateAllOverlays()",
v-on:click="openTabAuthorship(repo[0], repo, 0, isGroupMerged(getGroupName(repo)))"
)
@@ -65,6 +76,7 @@
)
span.tooltip-text Click to view group's code
a(
+ v-if="!isChartGroupWidgetMode",
onclick="deactivateAllOverlays()",
v-on:click="openTabZoom(repo[0], filterSinceDate, filterUntilDate, isGroupMerged(getGroupName(repo)))"
)
@@ -74,6 +86,25 @@
v-bind:class="{ 'active-icon': isSelectedTab(repo[0].name, repo[0].repoName, 'zoom', true) }"
)
span.tooltip-text Click to view breakdown of commits
+ a(
+ v-if="isChartGroupWidgetMode && !isChartWidgetMode",
+ v-bind:href="getReportLink()", target="_blank"
+ )
+ .tooltip
+ font-awesome-icon.icon-button(
+ icon="arrow-up-right-from-square",
+ )
+ span.tooltip-text(
+ v-if="!isChartGroupWidgetMode",
+ ) Click to view breakdown of commits on RepoSense
+ a(
+ v-if="!isChartGroupWidgetMode",
+ v-on:click="getEmbeddedIframe(i)"
+ )
+ .tooltip(v-bind:id="'tooltip-' + i")
+ font-awesome-icon.icon-button(icon="clipboard")
+ span.tooltip-text Click to copy iframe link for group
+
.tooltip.summary-chart__title--percentile(
v-if="sortGroupSelection.includes('totalCommits')"
) {{ getPercentile(i) }} % 
@@ -89,12 +120,17 @@
v-bind:style="{ 'color': fileTypeColors[fileType] }"
)
span {{ fileType }}
- .summary-chart(v-for="(user, j) in repo")
+ .summary-chart(
+ v-for="(user, j) in getRepo(repo)",
+ v-bind:style="isChartGroupWidgetMode && j === getRepo(repo).length - 1 ? {'marginBottom': 0} : {}",
+ v-bind:ref="'summary-chart-' + j"
+ )
.summary-chart__title(
v-if="!isGroupMerged(getGroupName(repo))",
- v-bind:class="{ 'active-background': user.name === activeUser && user.repoName === activeRepo }"
+ v-bind:class="{ 'active-background': user.name === activeUser && user.repoName === activeRepo \
+ && !isChartGroupWidgetMode }"
)
- .summary-chart__title--index {{ j+1 }}
+ .summary-chart__title--index(v-if="!isChartWidgetMode") {{ j+1 }}
.summary-chart__title--repo(v-if="filterGroupSelection === 'groupByNone'") {{ user.repoName }}
.summary-chart__title--author-repo(v-if="filterGroupSelection === 'groupByAuthors'") {{ user.repoName }}
.summary-chart__title--name(
@@ -104,21 +140,26 @@
.summary-chart__title--contribution.mini-font [{{ user.checkedFileTypeContribution }} lines]
a(
v-if="filterGroupSelection !== 'groupByRepos'",
- v-bind:class="!isBrokenLink(getRepoLink(repo[j])) ? '' : 'broken-link'",
- v-bind:href="getRepoLink(repo[j])", target="_blank"
+ v-bind:class="!isBrokenLink(getRepoLink(user)) ? '' : 'broken-link'",
+ v-bind:href="getRepoLink(user)", target="_blank"
)
.tooltip
font-awesome-icon.icon-button(:icon="getRepoIcon(repo[0])")
- span.tooltip-text {{getRepoLinkMessage(repo[j])}}
+ span.tooltip-text(
+ v-if="!isChartGroupWidgetMode",
+ ) {{getRepoLinkMessage(user)}}
a(
v-if="filterGroupSelection !== 'groupByAuthors'",
- v-bind:class="!isBrokenLink(getAuthorProfileLink(repo[j], repo[j].name)) ? '' : 'broken-link'",
- v-bind:href="getAuthorProfileLink(repo[j], repo[j].name)", target="_blank"
+ v-bind:class="!isBrokenLink(getAuthorProfileLink(user, user.name)) ? '' : 'broken-link'",
+ v-bind:href="getAuthorProfileLink(user, user.name)", target="_blank"
)
.tooltip
font-awesome-icon.icon-button(icon="user")
- span.tooltip-text {{getAuthorProfileLinkMessage(repo[j])}}
+ span.tooltip-text(
+ v-if="!isChartGroupWidgetMode",
+ ) {{getAuthorProfileLinkMessage(user)}}
a(
+ v-if="!isChartGroupWidgetMode",
onclick="deactivateAllOverlays()",
v-on:click="openTabAuthorship(user, repo, j, isGroupMerged(getGroupName(repo)))"
)
@@ -129,6 +170,7 @@
)
span.tooltip-text Click to view author's contribution.
a(
+ v-if="!isChartGroupWidgetMode",
onclick="deactivateAllOverlays()",
v-on:click="openTabZoom(user, filterSinceDate, filterUntilDate, isGroupMerged(getGroupName(repo)))"
)
@@ -138,6 +180,24 @@
v-bind:class="{ 'active-icon': isSelectedTab(user.name, user.repoName, 'zoom', false) }"
)
span.tooltip-text Click to view breakdown of commits
+ a(
+ v-if="isChartGroupWidgetMode",
+ v-bind:href="getReportLink()", target="_blank"
+ )
+ .tooltip
+ font-awesome-icon.icon-button(
+ icon="arrow-up-right-from-square",
+ )
+ span.tooltip-text(
+ v-if="!isChartGroupWidgetMode",
+ ) Click to view breakdown of commits on RepoSense
+ a(
+ v-if="!isChartGroupWidgetMode",
+ v-on:click="getEmbeddedIframe(i , j)"
+ )
+ .tooltip(v-bind:id="'tooltip-' + i + '-' + j")
+ font-awesome-icon.icon-button(icon="clipboard")
+ span.tooltip-text Click to copy iframe link
.tooltip.summary-chart__title--percentile(
v-if="filterGroupSelection === 'groupByNone' && sortGroupSelection.includes('totalCommits')"
) {{ getPercentile(j) }} % 
@@ -154,7 +214,8 @@
v-bind:udate="filterUntilDate",
v-bind:avgsize="avgCommitSize",
v-bind:mergegroup="isGroupMerged(getGroupName(repo))",
- v-bind:filtersearch="filterSearch")
+ v-bind:filtersearch="filterSearch",
+ v-bind:is-widget-mode="isChartGroupWidgetMode")
.overlay
.summary-chart__contribution
@@ -187,7 +248,7 @@ import { mapState } from 'vuex';
import brokenLinkDisabler from '../mixin/brokenLinkMixin';
import cRamp from './c-ramp.vue';
-import { User } from '../types/types';
+import { Repo, User } from '../types/types';
import { FilterGroupSelection, FilterTimeFrame, SortGroupSelection } from '../types/summary';
import { StoreState, ZoomInfo } from '../types/vuex.d';
import { AuthorFileTypeContributions } from '../types/zod/commits-type';
@@ -251,6 +312,14 @@ export default defineComponent({
type: String,
default: SortGroupSelection.GroupTitle,
},
+ chartGroupIndex: {
+ type: Number,
+ default: undefined,
+ },
+ chartIndex: {
+ type: Number,
+ default: undefined,
+ },
},
data() {
return {
@@ -267,7 +336,7 @@ export default defineComponent({
let totalCommits = 0;
let totalCount = 0;
this.filteredRepos.forEach((repo) => {
- repo.forEach((user) => {
+ repo.forEach((user: User) => {
user.commits?.forEach((slice) => {
if (slice.insertions > 0) {
totalCount += 1;
@@ -278,11 +347,19 @@ export default defineComponent({
});
return totalCommits / totalCount;
},
-
- filteredRepos(): User[][] {
- return this.filtered.filter((repo) => repo.length > 0);
+ filteredRepos() {
+ const repos = this.filtered.filter((repo) => repo.length > 0);
+ if (this.isChartGroupWidgetMode && this.chartGroupIndex! < repos.length) {
+ return [repos[this.chartGroupIndex!]];
+ }
+ return repos;
+ },
+ isChartGroupWidgetMode() {
+ return this.chartGroupIndex !== undefined && this.chartGroupIndex >= 0;
+ },
+ isChartWidgetMode() {
+ return this.chartIndex !== undefined && this.chartIndex >= 0 && this.isChartGroupWidgetMode;
},
-
...mapState({
mergedGroups: (state: unknown) => (state as StoreState).mergedGroups,
fileTypeColors: (state: unknown) => (state as StoreState).fileTypeColors,
@@ -487,6 +564,66 @@ export default defineComponent({
this.$store.commit('updateTabZoomInfo', info);
},
+ async getEmbeddedIframe(chartGroupIndex: number, chartIndex: number = -1) {
+ const isChartIndexProvided = chartIndex !== -1;
+ // Set height of iframe according to number of charts to avoid scrolling
+ let totalChartHeight = 0;
+ if (!isChartIndexProvided) {
+ totalChartHeight += (this.$refs[`summary-charts-${chartGroupIndex}`] as HTMLElement[])[0].clientHeight;
+ } else {
+ totalChartHeight += (this.$refs[`summary-chart-${chartIndex}`] as HTMLElement[])[0].clientHeight;
+ totalChartHeight += this.filterGroupSelection === 'groupByNone'
+ ? 0
+ : (this.$refs[`summary-charts-title-${chartGroupIndex}`] as HTMLElement[])[0].clientHeight;
+ }
+
+ const margins = 30;
+ const iframeStart = '`;
+ const [baseUrl, ...params] = window.location.href.split('?');
+ const groupIndexParam = isChartIndexProvided ? `&chartIndex=${chartIndex}` : '';
+ const url = `${baseUrl}#/widget/?${params.join('?')}&chartGroupIndex=${chartGroupIndex}${groupIndexParam}`;
+ const iframe = iframeStart + url + iframeEnd;
+ if (navigator.clipboard) {
+ navigator.clipboard.writeText(iframe);
+ } else {
+ // Clipboard API is not supported (non-secure origin of neither HTTPS nor localhost)
+ const textarea = document.createElement('textarea');
+ textarea.value = iframe;
+ textarea.setAttribute('readonly', '');
+ textarea.style.position = 'absolute';
+ textarea.style.left = '-9999px';
+ document.body.appendChild(textarea);
+ textarea.select();
+ document.execCommand('copy');
+ document.body.removeChild(textarea);
+ }
+ const tooltipId = `tooltip-${chartGroupIndex}${isChartIndexProvided ? `-${chartIndex}` : ''}`;
+ this.updateCopyTooltip(tooltipId, 'Copied iframe');
+ },
+ updateCopyTooltip(tooltipId: string, text: string) {
+ const tooltipElement = document.getElementById(tooltipId);
+ if (tooltipElement && tooltipElement.querySelector('.tooltip-text')) {
+ const tooltipTextElement = tooltipElement.querySelector('.tooltip-text');
+ const originalText = tooltipTextElement!.textContent;
+ tooltipElement.querySelector('.tooltip-text')!.textContent = text;
+ setTimeout(() => {
+ tooltipTextElement!.textContent = originalText;
+ }, 2000);
+ }
+ },
+ getReportLink() {
+ const url = window.location.href;
+ const regexToRemoveWidget = /([?&])((chartIndex|chartGroupIndex)=\d+)/g;
+ return url.replace(regexToRemoveWidget, '');
+ },
+ getRepo(repo: Repo[]) {
+ if (this.isChartGroupWidgetMode && this.isChartWidgetMode) {
+ return [repo[this.chartIndex!]];
+ }
+ return repo;
+ },
+
getBaseTarget(target: HTMLElement | null): HTMLElement | null {
if (!target) {
// Should never reach here - function assumes that target is a child of the div with class 'summary-chart__ramp'
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index 62f086abbd..145ead4415 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -8,6 +8,13 @@ const routes: Array = [
path: '/',
component: Home,
},
+ {
+ path: '/widget',
+ // route level code-splitting
+ // this generates a separate chunk (about.[hash].js) for this route
+ // which is lazy-loaded when the route is visited.
+ component: () => import('../views/c-widget.vue'),
+ },
];
const router: Router = createRouter({
diff --git a/frontend/src/styles/style.scss b/frontend/src/styles/style.scss
index ffa8e55516..e3a6789c60 100644
--- a/frontend/src/styles/style.scss
+++ b/frontend/src/styles/style.scss
@@ -51,6 +51,10 @@ a.broken-link {
padding: 2rem 1.5rem;
}
+.widget-padding {
+ padding: .5rem 1rem;
+}
+
.warn {
color: mui-color('red');
diff --git a/frontend/src/styles/summary-chart.scss b/frontend/src/styles/summary-chart.scss
new file mode 100644
index 0000000000..edcc96774e
--- /dev/null
+++ b/frontend/src/styles/summary-chart.scss
@@ -0,0 +1,228 @@
+@import 'colors';
+@import 'z-indices';
+
+/* Summary */
+#summary {
+ .summary-status {
+ text-align: center;
+ }
+
+ @mixin icon-button-config {
+ color: mui-color('grey');
+ padding: 0 1.2px 0 1.2px;
+ text-decoration: none;
+ }
+
+ .icon-button {
+ @include icon-button-config;
+ cursor: pointer;
+ }
+
+ .broken-link {
+ .icon-button {
+ cursor: default;
+ }
+ }
+
+ .summary-picker {
+ align-items: center;
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: center;
+ margin-bottom: 2rem;
+
+ &__section {
+ align-items: inherit;
+ display: flex;
+ flex: 0 1 auto;
+ flex-flow: inherit;
+ justify-content: inherit;
+ }
+
+ &__checkboxes {
+ label {
+ margin-left: .5rem;
+ }
+
+ span {
+ margin-left: .25rem;
+ }
+ }
+
+ .mui-textfield,
+ .mui-select {
+ @include small-font;
+ margin: .5rem;
+ padding-right: 10px;
+ }
+
+ .mui-btn {
+ @include small-font;
+ background: transparent;
+ box-shadow: none;
+ color: mui-color('grey');
+ font-weight: bold;
+ left: -8px;
+ margin: 0;
+ padding: 0;
+ vertical-align: middle;
+ }
+
+ .search_box {
+ align-items: center;
+ display: flex;
+ }
+
+ input {
+ @include small-font;
+ padding-right: 10px;
+ }
+
+ label {
+ @include small-font;
+ overflow-y: hidden;
+ text-align: left;
+ width: fit-content;
+ }
+
+ input,
+ select {
+ @include small-font;
+ }
+ }
+
+ .summary-charts {
+ margin-bottom: 1.4rem;
+
+ &__title {
+ align-items: center;
+ display: flex;
+ font-weight: bold;
+ text-align: left;
+
+ & > * {
+ padding-right: .5rem;
+ }
+
+ &--index {
+ background: mui-color('black');
+ color: mui-color('white');
+ @include medium-font;
+ overflow: hidden;
+ padding: .1em .25em;
+ vertical-align: middle;
+ }
+
+ &--groupname {
+ @include medium-font;
+ padding: .5rem;
+ }
+
+ &--percentile {
+ @include mini-font;
+ color: mui-color('grey');
+ margin-left: auto;
+ }
+
+ &--contribution {
+ @include mini-font;
+ display: inline;
+ }
+ }
+
+ &__fileType--breakdown {
+ overflow-y: hidden;
+
+ &__legend {
+ @include small-font;
+ display: inline;
+ float: left;
+ }
+ }
+ }
+
+ .summary-chart {
+ display: inline-block;
+ margin-bottom: 1rem;
+ position: relative;
+ text-align: left;
+ width: 100%;
+
+ &__title {
+ align-items: center;
+ clear: left;
+ display: flex;
+
+ & > * {
+ padding-right: .5rem;
+ }
+
+ &--index {
+ margin-left: 3px;
+ }
+
+ &--repo {
+ font-weight: bold;
+ }
+
+ &--index::after {
+ content: '.';
+ }
+
+ &--repo {
+ padding-right: .25rem;
+ }
+
+ &--contribution {
+ @include mini-font;
+ }
+
+ &--percentile {
+ @include mini-font;
+ color: mui-color('grey');
+ margin-left: auto;
+ padding-right: 0;
+ }
+ }
+
+ &__ramp {
+ position: relative;
+
+ .overlay {
+ height: 100%;
+ position: absolute;
+ top: 0;
+
+ &.show {
+ background-color: rgba(mui-color('white'), .5);
+ border: 1px dashed mui-color('black');
+ }
+
+ &.edge {
+ border-right: 1px dashed mui-color('black');
+ }
+ }
+ }
+
+ &__contrib {
+ text-align: left;
+
+ &--bar {
+ background-color: mui-color('red');
+ float: left;
+ height: 4px;
+ margin-top: 2px;
+ }
+ }
+ }
+
+ .active-icon {
+ background-color: mui-color('green');
+ border-radius: 2px;
+ color: mui-color('white');
+ }
+
+ .active-background {
+ background-color: mui-color('yellow', '200');
+ }
+}
diff --git a/frontend/src/utils/load-font-awesome-icons.js b/frontend/src/utils/load-font-awesome-icons.js
index c1f402915a..e74d15b397 100644
--- a/frontend/src/utils/load-font-awesome-icons.js
+++ b/frontend/src/utils/load-font-awesome-icons.js
@@ -4,7 +4,7 @@ import {
faChevronDown, faChevronUp, faCircle, faCode, faCodeMerge,
faEllipsisH, faExclamation, faHistory, faListUl,
faPlusCircle, faSpinner, faTags, faUser, faUserEdit,
- faDatabase,
+ faDatabase, faClipboard, faArrowUpRightFromSquare,
} from '@fortawesome/free-solid-svg-icons';
import {
@@ -32,6 +32,8 @@ library.add(
faGithub,
faGitlab,
faBitbucket,
+ faClipboard,
+ faArrowUpRightFromSquare,
);
// c-zoom
diff --git a/frontend/src/views/c-home.vue b/frontend/src/views/c-home.vue
index aa6dcd6789..87a21d82cc 100644
--- a/frontend/src/views/c-home.vue
+++ b/frontend/src/views/c-home.vue
@@ -1,14 +1,5 @@
#home
- 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 }}
-
template(v-if="userUpdated")
c-resizer
template(v-slot:left)
@@ -76,209 +67,60 @@
+
+