Merge pull request #3395 from ethereum/sol2uml-download-matomo

Add download Button and Matomo Integration to Sol2Uml plugin
pull/5370/head
Joseph Izang 2 years ago committed by GitHub
commit 94b5eec640
  1. 7
      apps/remix-ide/src/app/plugins/contractFlattener.tsx
  2. 6
      apps/remix-ide/src/app/plugins/solidity-umlgen.tsx
  3. 4
      apps/remix-ide/src/app/tabs/locales/en/solUmlgen.json
  4. 4
      apps/remix-ide/src/app/tabs/locales/zh/solUmlgen.json
  5. 143
      libs/remix-ui/solidity-uml-gen/src/lib/components/UmlDownload.tsx
  6. 28
      libs/remix-ui/solidity-uml-gen/src/lib/css/solidity-uml-gen.css
  7. 38
      libs/remix-ui/solidity-uml-gen/src/lib/solidity-uml-gen.tsx
  8. 86
      libs/remix-ui/solidity-uml-gen/src/lib/utilities/UmlDownloadStrategy.ts
  9. 5
      package.json
  10. 45
      yarn.lock

@ -3,6 +3,8 @@ import { Plugin } from '@remixproject/engine'
import { customAction } from '@remixproject/plugin-api'
import { concatSourceFiles, getDependencyGraph } from '@remix-ui/solidity-compiler'
const _paq = window._paq = window._paq || []
const profile = {
name: 'contractflattener',
displayName: 'Contract Flattener',
@ -22,6 +24,7 @@ export class ContractFlattener extends Plugin {
this.on('solidity', 'compilationFinished', async (file, source, languageVersion, data, input, version) => {
await this.flattenContract(source, this.fileName, data)
})
_paq.push(['trackEvent', 'plugin', 'activated', 'contractFlattener'])
}
async flattenAContract(action: customAction) {
@ -33,7 +36,7 @@ export class ContractFlattener extends Plugin {
* Takes currently compiled contract that has a bunch of imports at the top
* and flattens them ready for UML creation. Takes the flattened result
* and assigns to a local property
* @returns {Promise<string>}
* @returns {Promise<void>}
*/
async flattenContract (source: any, filePath: string, data: any) {
const ast = data.sources
@ -44,6 +47,6 @@ export class ContractFlattener extends Plugin {
const sources = source.sources
const result = concatSourceFiles(sorted, sources)
await this.call('fileManager', 'writeFile', `${filePath}_flattened.sol`, result)
return result
_paq.push(['trackEvent', 'plugin', 'contractFlattener', 'flattenAContract'])
}
}

@ -13,6 +13,8 @@ import { PluginViewWrapper } from '@remix-ui/helper'
import { customAction } from '@remixproject/plugin-api'
const parser = (window as any).SolidityParser
const _paq = window._paq = window._paq || []
const profile = {
name: 'solidityumlgen',
displayName: 'Solidity UML Generator',
@ -81,6 +83,7 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen {
const umlDot = convertUmlClasses2Dot(umlClasses)
const payload = vizRenderStringSync(umlDot)
this.updatedSvg = payload
_paq.push(['trackEvent', 'solidityumlgen', 'umlgenerated'])
this.renderComponent()
await this.call('tabs', 'focus', 'solidityumlgen')
} catch (error) {
@ -115,6 +118,7 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen {
generateCustomAction = async (action: customAction) => {
this.updatedSvg = this.updatedSvg.startsWith('<?xml') ? '' : this.updatedSvg
this.currentFile = action.path[0]
_paq.push(['trackEvent', 'solidityumlgen', 'activated'])
await this.generateUml(action.path[0])
}
@ -169,6 +173,7 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen {
loading: this.loading,
themeSelected: this.currentlySelectedTheme,
themeName: this.themeName,
fileName: this.currentFile,
themeCollection: this.themeCollection
})
}
@ -179,6 +184,7 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen {
loading={state.loading}
themeSelected={state.currentlySelectedTheme}
themeName={state.themeName}
fileName={state.fileName}
themeCollection={state.themeCollection}
/>
}

@ -0,0 +1,4 @@
{
"solUml.pngDownload": "Download as PNG",
"solUml.pdfDownload": "Download as PDF"
}

@ -0,0 +1,4 @@
{
"solUml.PngDownload": "sfg",
"solUml.PdfDownload": "sdf"
}

@ -0,0 +1,143 @@
import { CustomTooltip } from '@remix-ui/helper'
import React, { Fragment, Ref } from 'react'
import { Dropdown } from 'react-bootstrap'
import { UmlFileType } from '../utilities/UmlDownloadStrategy'
const _paq = (window._paq = window._paq || [])
export const Markup = React.forwardRef(
(
{
children,
onClick,
icon,
className = "",
}: {
children: React.ReactNode
onClick: (e) => void
icon: string
className: string
},
ref: Ref<HTMLButtonElement>
) => (
<button
ref={ref}
onClick={(e) => {
e.preventDefault()
onClick(e)
}}
className={className.replace("dropdown-toggle", "")}
>
<i className={icon}></i>
</button>
)
)
export const UmlCustomMenu = React.forwardRef(
(
{
children,
style,
className,
"aria-labelledby": labeledBy,
}: {
children: React.ReactNode
style?: React.CSSProperties
className: string
"aria-labelledby"?: string
},
ref: Ref<HTMLDivElement>
) => {
const height = window.innerHeight * 0.6
return (
<div
ref={ref}
style={style}
className={className}
aria-labelledby={labeledBy}
>
<ul
className="overflow-auto list-unstyled mb-0"
style={{ maxHeight: height + "px" }}
>
{children}
</ul>
</div>
)
}
)
interface UmlDownloadProps {
download: (fileType: UmlFileType) => void
}
export default function UmlDownload(props: UmlDownloadProps) {
return (
<Fragment>
<Dropdown id="solUmlMenuDropdown">
<Dropdown.Toggle
icon="far fa-arrow-to-bottom uml-btn-icon"
as={Markup}
className="badge badge-info remixui_no-shadow p-2 rounded-circle mr-2"
></Dropdown.Toggle>
<Dropdown.Menu as={UmlCustomMenu} className="custom-dropdown-items">
<Dropdown.Item
onClick={() => {
_paq.push([
"trackEvent",
"solidityumlgen",
"download",
"downloadAsPng",
]);
props.download("png")
}}
>
<CustomTooltip
placement="left-start"
tooltipId="solUmlgenDownloadAsPngTooltip"
tooltipClasses="text-nowrap"
tooltipText={"Download UML diagram as a PNG file"}
>
<div data-id="umlPngDownload">
<span
id="umlPngDownloadBtn"
data-id="umlPngDownload"
className="far fa-image pl-2"
></span>
<span className="pl-1">Download as PNG</span>
</div>
</CustomTooltip>
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
onClick={() => {
_paq.push([
"trackEvent",
"solUmlgen",
"download",
"downloadAsPdf",
]);
props.download("pdf")
}}
>
<CustomTooltip
placement="left-start"
tooltipId="solUmlgenDownloadAsPdfTooltip"
tooltipClasses="text-nowrap"
tooltipText={"Download UML diagram as a PDF file"}
>
<div data-id="umlPdfDownload">
<span
id="umlPdfDownloadBtn"
data-id="umlPdfDownload"
className="far fa-file-pdf pl-2"
></span>
<span className="pl-2">Download as PDF</span>
</div>
</CustomTooltip>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</Fragment>
)
}

@ -7,3 +7,31 @@
border-style: solid;
border-color: var(--info);
}
#solUmlMenuDropdown > div > ul > a:hover {
background-color: var(--secondary);
border-radius: 2px;
color: var(--text)
}
.custom-dropdown-items {
padding: 0.25rem 0.25rem;
border-radius: .25rem;
background: var(--custom-select);
}
.custom-dropdown-items a {
border-radius: .25rem;
text-transform: none;
text-decoration: none;
font-weight: normal;
font-size: 0.875rem;
padding: 0.25rem 0.25rem;
width: auto;
color: var(--text);
}
.uml-btn-icon {
width: 0.5rem;
height: 0.5rem;
}

@ -1,12 +1,15 @@
import React, { Fragment, useEffect, useState } from 'react'
import React, { Fragment, useCallback, useEffect, useState } from 'react'
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'
import { ThemeSummary } from '../types'
import UmlDownload from './components/UmlDownload'
import './css/solidity-uml-gen.css'
import { UmlDownloadContext, UmlFileType } from './utilities/UmlDownloadStrategy'
export interface RemixUiSolidityUmlGenProps {
updatedSvg?: string
loading?: boolean
themeSelected?: string
themeName: string
fileName: string
themeCollection: ThemeSummary[]
}
@ -18,24 +21,32 @@ interface ActionButtonsProps {
}
}
export function RemixUiSolidityUmlGen ({ updatedSvg, loading }: RemixUiSolidityUmlGenProps) {
let umlCopy = ''
export function RemixUiSolidityUmlGen ({ updatedSvg, loading, fileName }: RemixUiSolidityUmlGenProps) {
const [showViewer, setShowViewer] = useState(false)
const [validSvg, setValidSvg] = useState(false)
const umlDownloader = new UmlDownloadContext()
useEffect(() => {
if (updatedSvg.startsWith('<?xml') && updatedSvg.includes('<svg')) {
umlCopy = updatedSvg
}
setValidSvg (updatedSvg.startsWith('<?xml') && updatedSvg.includes('<svg'))
setShowViewer(updatedSvg.startsWith('<?xml') && updatedSvg.includes('<svg'))
}
, [updatedSvg])
}, [updatedSvg])
const encoder = new TextEncoder()
const data = encoder.encode(updatedSvg)
const final = btoa(String.fromCharCode.apply(null, data))
const download = useCallback((fileType: UmlFileType) => {
if (umlCopy.length === 0) {
return
}
umlDownloader.download(umlCopy, fileName, fileType)
}, [updatedSvg, fileName])
function ActionButtons({ actions: { zoomIn, zoomOut, resetTransform }}: ActionButtonsProps) {
return (
@ -46,29 +57,24 @@ export function RemixUiSolidityUmlGen ({ updatedSvg, loading }: RemixUiSolidityU
style={{ zIndex: 3, top: "10", right: "2em" }}
>
<div className="py-2 px-2 d-flex justify-content-center align-items-center">
<button
className="btn btn-outline-info d-none rounded-circle mr-2"
onClick={() => resetTransform()}
>
<i className="far fa-arrow-to-bottom align-item-center d-flex justify-content-center"></i>
</button>
<UmlDownload download={download} />
<button
className="badge badge-info remixui_no-shadow p-2 rounded-circle mr-2"
onClick={() => zoomIn()}
>
<i className="far fa-plus "></i>
<i className="far fa-plus uml-btn-icon"></i>
</button>
<button
className="badge badge-info remixui_no-shadow p-2 rounded-circle mr-2"
onClick={() => zoomOut()}
>
<i className="far fa-minus align-item-center d-flex justify-content-center"></i>
<i className="far fa-minus uml-btn-icon"></i>
</button>
<button
className="badge badge-info remixui_no-shadow p-2 rounded-circle mr-2"
onClick={() => resetTransform()}
>
<i className="far fa-undo align-item-center d-flex justify-content-center"></i>
<i className="far fa-undo uml-btn-icon"></i>
</button>
</div>
</div>

@ -0,0 +1,86 @@
interface IUmlDownloadStrategy {
download (uml: string, fileName: string): void
}
export type UmlFileType = 'pdf' | 'png'
class PdfUmlDownloadStrategy implements IUmlDownloadStrategy {
public download (uml: string, fileName: string): void {
const svg = new Blob([uml], { type: 'image/svg+xml;charset=utf-8' })
const Url = window.URL || window.webkitURL
const url = Url.createObjectURL(svg)
const img = document.createElement('img')
let doc
img.onload = async () => {
const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
const ctx = canvas.getContext('2d')
const scale = window.devicePixelRatio*1
canvas.style.width = `${Math.round(img.naturalWidth/scale)}`.concat('px')
canvas.style.height = `${Math.round(img.naturalHeight/scale)}`.concat('px')
canvas.style.margin = '0'
canvas.style.padding = '0'
ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
ctx.drawImage(img, 0, 0, Math.round(img.naturalWidth/scale), Math.round(img.naturalHeight/scale))
if (doc === null || doc === undefined) {
const { default: jsPDF } = await import('jspdf')
doc = new jsPDF('landscape', 'px', [img.naturalHeight, img.naturalWidth], true)
}
const pageWidth = doc.internal.pageSize.getWidth()
const pageHeight = doc.internal.pageSize.getHeight()
doc.addImage(canvas.toDataURL('image/png',0.5), 'PNG', 0, 0, pageWidth, pageHeight)
doc.save(fileName.split('/')[1].split('.')[0].concat('.pdf'))
}
img.src = url
doc = null
}
}
class ImageUmlDownloadStrategy implements IUmlDownloadStrategy {
public download (uml: string, fileName: string): void {
const svg = new Blob([uml], { type: 'image/svg+xml;charset=utf-8' })
const Url = window.URL || window.webkitURL
const url = Url.createObjectURL(svg)
const img = document.createElement('img')
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
const ctx = canvas.getContext('2d')
const scale = window.devicePixelRatio*1
canvas.style.width = `${Math.round(img.naturalWidth/scale)}`.concat('px')
canvas.style.height = `${Math.round(img.naturalHeight/scale)}`.concat('px')
canvas.style.margin = '0'
canvas.style.padding = '0'
ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
ctx.drawImage(img, 0, 0, Math.round(img.naturalWidth/scale), Math.round(img.naturalHeight/scale))
const png = canvas.toDataURL('image/png')
const a = document.createElement('a')
a.download = fileName.split('/')[1].split('.')[0].concat('.png')
a.href = png
a.click()
}
img.src = url
}
}
export class UmlDownloadContext {
private strategy: IUmlDownloadStrategy
private setStrategy (strategy: IUmlDownloadStrategy): void {
this.strategy = strategy
}
public download (uml: string, fileName: string, fileType: UmlFileType ): void {
if (fileType === 'pdf') {
this.setStrategy(new PdfUmlDownloadStrategy())
} else if (fileType === 'png') {
this.setStrategy(new ImageUmlDownloadStrategy())
} else {
throw new Error('Invalid file type')
}
this.strategy.download(uml, fileName)
}
}

@ -153,7 +153,6 @@
"core-js": "^3.6.5",
"deep-equal": "^1.0.1",
"document-register-element": "1.13.1",
"dom-to-pdf": "^0.3.1",
"eslint-config-prettier": "^8.5.0",
"ethers": "^5.4.2",
"ethjs-util": "^0.1.6",
@ -171,6 +170,7 @@
"isomorphic-git": "^1.8.2",
"jquery": "^3.3.1",
"js-yaml": "^4.1.0",
"jspdf": "^2.5.1",
"jszip": "^3.6.0",
"latest-version": "^5.1.0",
"merge": "^2.1.1",
@ -189,12 +189,13 @@
"react-multi-carousel": "^2.8.2",
"react-router-dom": "^6.3.0",
"react-tabs": "^3.2.2",
"react-zoom-pan-pinch": "^2.2.0",
"react-zoom-pan-pinch": "^3.0.2",
"regenerator-runtime": "0.13.7",
"rss-parser": "^3.12.0",
"signale": "^1.4.0",
"sol2uml": "^2.4.3",
"string-similarity": "^4.0.4",
"svg2pdf.js": "^2.2.1",
"swarmgw": "^0.3.1",
"time-stamp": "^2.2.0",
"toml": "^3.0.0",

@ -11473,18 +11473,6 @@ dom-serializer@^2.0.0:
domhandler "^5.0.2"
entities "^4.2.0"
"dom-to-image@https://github.com/dmapper/dom-to-image":
version "2.6.0"
resolved "https://github.com/dmapper/dom-to-image#a7c386a8ea813930f05449ac71ab4be0c262dff3"
dom-to-pdf@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/dom-to-pdf/-/dom-to-pdf-0.3.1.tgz#06db966acc73a7b81ce183202f7b6ff1f5f38578"
integrity sha512-2duxMNttyQr5XySV2t7gftCVhHr+zoE/f1h0nQDp/1KugAsk07EkZ9zcPFgZihcyj4dXBUWyVSxOik8czuFGDQ==
dependencies:
dom-to-image "https://github.com/dmapper/dom-to-image"
jspdf "^2.5.1"
dom-walk@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
@ -13344,6 +13332,11 @@ follow-redirects@^1.15.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
font-family-papandreou@^0.2.0-patch1:
version "0.2.0-patch2"
resolved "https://registry.yarnpkg.com/font-family-papandreou/-/font-family-papandreou-0.2.0-patch2.tgz#c75b659e96ffbc7ab2af651cf7b4910b334e8dd2"
integrity sha512-l/YiRdBSH/eWv6OF3sLGkwErL+n0MqCICi9mppTZBOCL5vixWGDqCYvRcuxB2h7RGCTzaTKOHT2caHvCXQPRlw==
for-each@~0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@ -21944,10 +21937,10 @@ react-transition-group@^4.4.1:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-zoom-pan-pinch@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/react-zoom-pan-pinch/-/react-zoom-pan-pinch-2.2.0.tgz#15dd97aef798699016e4e30182cc51c4bddd4739"
integrity sha512-khOlTeTI/ZXtbCfqUmkKW0HpM+w0RklEQ1DlFVi0D9y90r+Z8x+ipKBXvPQC3rUu5VoYK4603SY8GsA6enfa8w==
react-zoom-pan-pinch@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.0.2.tgz#706c67e875e9a30480cdbef8dd4e3d6fdac9921c"
integrity sha512-c8BxPl/zK6RiOYrV/xBQ+ebgZpsMvbz6WOoqv2P/1QWxGCk1+q3xWF+5ub4QYasv4W8+J6vSelOR8H0WCEbL4w==
react@^17.0.2:
version "17.0.2"
@ -23804,6 +23797,11 @@ spdy@^4.0.2:
select-hose "^2.0.0"
spdy-transport "^3.0.0"
specificity@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019"
integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==
split-on-first@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
@ -24408,6 +24406,16 @@ svg-pathdata@^6.0.3:
resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac"
integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==
svg2pdf.js@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/svg2pdf.js/-/svg2pdf.js-2.2.1.tgz#fa81849be57c5a405c8394d35e4a4ea8aaaebeb5"
integrity sha512-gJsFT42tb+pYTuFudkKgpMws54DvsJW7wmzGRUY1b9CUJpRMoBU5B4HrCMUTlK2lpcdPL5cOyr84hy2BEj1/Ag==
dependencies:
cssesc "^3.0.0"
font-family-papandreou "^0.2.0-patch1"
specificity "^0.4.1"
svgpath "^2.3.0"
svgo@^2.7.0, svgo@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
@ -24421,6 +24429,11 @@ svgo@^2.7.0, svgo@^2.8.0:
picocolors "^1.0.0"
stable "^0.1.8"
svgpath@^2.3.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/svgpath/-/svgpath-2.6.0.tgz#5b160ef3d742b7dfd2d721bf90588d3450d7a90d"
integrity sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg==
swarm-js@^0.1.40:
version "0.1.40"
resolved "https://registry.yarnpkg.com/swarm-js/-/swarm-js-0.1.40.tgz#b1bc7b6dcc76061f6c772203e004c11997e06b99"

Loading…
Cancel
Save