diff --git a/client/run-telepresence.sh b/client/run-telepresence.sh index 5b94efbfbb..4d04933744 100755 --- a/client/run-telepresence.sh +++ b/client/run-telepresence.sh @@ -25,7 +25,7 @@ Some deployment-specific information will be read from the your values.yaml file TEMPLATES='{"custom":true,"repositories": [{"name":"Renku","ref":"master", "url":"https://github.com/SwissDataScienceCenter/renku-project-template"}, -{"name":"Telepresence","ref":"0.1.11", +{"name":"Telepresence","ref":"0.2.1", "url":"https://github.com/SwissDataScienceCenter/renku-project-template"}]}' PREVIEW_THRESHOLD='{"soft":"1048576","hard":"10485760"}' UPLOAD_THRESHOLD='{"soft":"104857600"}' diff --git a/client/src/model/RenkuModels.js b/client/src/model/RenkuModels.js index 9c275e311b..98fb9afea3 100644 --- a/client/src/model/RenkuModels.js +++ b/client/src/model/RenkuModels.js @@ -196,6 +196,7 @@ const newProjectSchema = new Schema({ [Prop.SCHEMA]: new Schema({ title: { [Prop.INITIAL]: "", [Prop.MANDATORY]: true }, titlePristine: { [Prop.INITIAL]: true, [Prop.MANDATORY]: true }, + description: { [Prop.INITIAL]: "", [Prop.MANDATORY]: true }, namespace: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, namespacePristine: { [Prop.INITIAL]: true, [Prop.MANDATORY]: true }, visibility: { [Prop.INITIAL]: "", [Prop.MANDATORY]: true }, @@ -214,6 +215,7 @@ const newProjectSchema = new Schema({ data: { [Prop.SCHEMA]: new Schema({ title: { [Prop.INITIAL]: "", [Prop.MANDATORY]: false }, + description: { [Prop.INITIAL]: "", [Prop.MANDATORY]: true }, namespace: { [Prop.INITIAL]: "", [Prop.MANDATORY]: false }, visibility: { [Prop.INITIAL]: "", [Prop.MANDATORY]: false }, template: { [Prop.INITIAL]: "", [Prop.MANDATORY]: false }, diff --git a/client/src/project/new/Project.style.css b/client/src/project/new/Project.style.css index a4b4801884..616eeb6184 100644 --- a/client/src/project/new/Project.style.css +++ b/client/src/project/new/Project.style.css @@ -94,3 +94,24 @@ svg.no-pointer { display: none !important; content: none !important; } + +.template-card { + cursor: pointer; +} + +.template-card:hover { + border-color: #009568; +} +.template-card:hover .card-footer { + border-color: #009568; + color: #009568; + background-color: white; +} +.template-card.selected { + border: 3px solid #009568; + color: #009568; +} + +.template-card img { + height: 60px; +} diff --git a/client/src/project/new/ProjectNew.container.js b/client/src/project/new/ProjectNew.container.js index ca172b1884..02cda6cdcf 100644 --- a/client/src/project/new/ProjectNew.container.js +++ b/client/src/project/new/ProjectNew.container.js @@ -459,7 +459,7 @@ class NewProject extends Component { if (creation.created) { this.refreshUserProjects(); if (!creation.kgError && !creation.projectError) { - const slug = `${creation.newNamespace}/${creation.newName}`; + const slug = `${creation.newNamespace}/${creation.newNameSlug}`; this.props.history.push(`/projects/${slug}`); } } diff --git a/client/src/project/new/ProjectNew.present.js b/client/src/project/new/ProjectNew.present.js index 8c76716f9e..51814965d2 100644 --- a/client/src/project/new/ProjectNew.present.js +++ b/client/src/project/new/ProjectNew.present.js @@ -28,17 +28,22 @@ import React, { Component, Fragment, useState, useEffect } from "react"; import { Link } from "react-router-dom"; import Autosuggest from "react-autosuggest"; import { - Alert, Button, ButtonGroup, Col, DropdownItem, Fade, Form, FormFeedback, FormGroup, FormText, Input, Label, - Modal, ModalBody, ModalFooter, ModalHeader, Row, Table, UncontrolledTooltip + Alert, Button, ButtonGroup, Card, CardBody, CardText, CardFooter, Col, DropdownItem, Fade, Form, + FormFeedback, FormGroup, FormText, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, + UncontrolledPopover, PopoverHeader, PopoverBody, Row, Table, UncontrolledTooltip } from "reactstrap"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faExclamationTriangle, faInfoCircle, faLink, faSyncAlt } from "@fortawesome/free-solid-svg-icons"; +import { + faExclamationTriangle, faInfoCircle, faLink, faQuestionCircle, faSyncAlt +} from "@fortawesome/free-solid-svg-icons"; import { ButtonWithMenu, Clipboard, ExternalLink, FieldGroup, Loader } from "../../utils/UIComponents"; -import { slugFromTitle } from "../../utils/HelperFunctions"; +import { simpleHash, slugFromTitle } from "../../utils/HelperFunctions"; import { capitalize } from "../../utils/formgenerator/FormGenerator.present"; import { Url } from "../../utils/url"; + import "./Project.style.css"; +import defaultTemplateIcon from "./templatePlaceholder.svg"; /** @@ -206,6 +211,7 @@ class NewProject extends Component {
+ <Description {...this.props} /> <Namespaces {...this.props} /> <Home {...this.props} /> <Visibility {...this.props} /> @@ -358,6 +364,19 @@ class Title extends Component { } } +function Description(props) { + const { handlers, meta, input } = props; + const error = meta.validation.errors["description"]; + + return ( + <FieldGroup id="description" type="text" label="Description" + value={input.description} + placeholder="A short project description" + feedback={error} invalid={error && !input.descriptionPristine} + onChange={(e) => handlers.setProperty("description", e.target.value)} /> + ); +} + class Namespaces extends Component { async componentDidMount() { // fetch namespaces if not available yet @@ -374,7 +393,7 @@ class Namespaces extends Component { const main = namespaces.fetching ? (<Fragment> <br /> - <Label className="font-italic">Refreshing... <Loader inline={true} size={16} /></Label> + <Label className="font-italic d-block">Refreshing... <Loader inline={true} size={16} /></Label> </Fragment>) : (<NamespacesAutosuggest {...this.props} />); const { list } = namespaces; @@ -640,13 +659,13 @@ class KnowledgeGraph extends Component { ); return ( <FormGroup> - <Label check style={{ marginLeft: "1.25rem" }}> - <Input id="knowledgeGraph" type="checkbox" + <Label check> + <Input id="knowledgeGraph" type="checkbox" className="me-2" checked={!this.props.input.knowledgeGraph} onChange={(e) => handlers.setProperty("knowledgeGraph", !e.target.checked)} /> Opt-out from Knowledge Graph </Label> - <FormText> + <FormText className="d-block"> <FontAwesomeIcon className="no-pointer" icon={faInfoCircle} /> The {kgLink} may make some metadata public, opt-out if this is not acceptable. </FormText> @@ -771,10 +790,10 @@ class Template extends Component { } render() { - const { handlers, input, templates, meta } = this.props; + const { config, handlers, input, templates, meta } = this.props; const error = meta.validation.errors["template"]; const invalid = error && !input.templatePristine ? true : false; - let main, help = null; + let main = null; if ((!input.userRepo && templates.fetching) || (input.userRepo && meta.userTemplates.fetching)) { main = ( <Fragment> @@ -792,35 +811,141 @@ class Template extends Component { ); } else { - const listedTemplates = input.userRepo ? - meta.userTemplates : - templates; - const options = listedTemplates.all.map(t => ( - <option key={t.id} value={t.id}>{`${t.parentRepo} / ${t.name}`}</option>) - ); + // Pass down templates and repository with the same format to the gallery component + let listedTemplates, repositories; + if (input.userRepo) { + listedTemplates = meta.userTemplates.all; + repositories = [{ url: meta.userTemplates.url, ref: meta.userTemplates.ref, name: "Custom" }]; + } + else { + listedTemplates = templates.all; + repositories = config.repositories; + } + + const select = (template) => handlers.setProperty("template", template); main = ( - <Input id="template" type="select" placeholder="Select template..." className="custom-select" - value={input.template} feedback={error} invalid={invalid} - onChange={(e) => handlers.setProperty("template", e.target.value)} > - <option key="" value="" disabled>Select a template...</option> - {options} - </Input> + <TemplateGallery + // error={error && invalid} // ? we may consider adding a more prominent underlining for errors + repositories={repositories} + select={select} + selected={input.template} + templates={listedTemplates} + /> ); - if (input.template) - help = capitalize(listedTemplates.all.filter(t => t.id === input.template)[0].description); } return ( <FormGroup> <Label>Template</Label> + {error && invalid && <div className="text-danger small">{error}</div>} {main} - {error && <FormFeedback {...invalid}>{error}</FormFeedback>} - {help && <FormText color="muted">{help}</FormText>} </FormGroup> ); } } +function TemplateGallery(props) { + const { repositories, select, selected, templates } = props; + + // One GalleryRow for each source + const gallery = repositories.map((repository) => { + const repoTitle = repository.name; + const repoTemplates = templates.filter(t => t.parentRepo === repoTitle); + const repoKey = simpleHash(repository.url + repository.ref); + return ( + <TemplateGalleryRow + key={repoKey} + repository={repository} + select={select} + selected={selected} + templates={repoTemplates} + /> + ); + }); + + return (<div>{gallery}</div>); +} + +// Show a link when we have a valid url. Otherwise, just simple text +function TemplateRepositoryLink(props) { + const { url } = props; + let repoUrl = url && url.length && url.startsWith("http") ? + url : + ""; + if (repoUrl.endsWith(".git")) + repoUrl = repoUrl.substring(repoUrl.length - 4); + const repoLink = repoUrl ? + (<ExternalLink url={repoUrl} title={url} role="link" />) : + url; + return repoLink; +} + +function TemplateGalleryRow(props) { + const { repository, select, selected, templates } = props; + + // Don't render anything if there are no templates for the repository + if (!templates || !templates.length) + return null; + + // Show a card for each template + const elements = templates.map(t => { + const imgSrc = t.icon ? + `data:image/png;base64,${t.icon}` : + defaultTemplateIcon; + const id = "id" + simpleHash(repository.name) + simpleHash(t.id); + const selectedClass = selected === t.id ? + "selected" : + ""; + + return ( + <Col key={t.id}> + <Card id={id} className={`template-card mb-2 text-center ${selectedClass}`} + onClick={() => { select(t.id); }}> + <CardBody className="p-1"> + <img src={imgSrc} alt={t.id + " template image"} /> + </CardBody> + <CardFooter className="p-1"> + <CardText className="small">{t.name}</CardText> + </CardFooter> + </Card> + <UncontrolledTooltip key="tooltip" placement="bottom" target={id}> + {t.description} + </UncontrolledTooltip> + </Col> + ); + }); + + // Add a title with information about the source repository + const repositoryInfoId = `info-${repository.name}`; + const title = ( + <Row> + <p className="fst-italic mt-2 mb-1"> + Source: {repository.name} + <FontAwesomeIcon id={repositoryInfoId} className="ms-2" icon={faQuestionCircle} /> + </p> + <UncontrolledPopover target={repositoryInfoId} trigger="legacy" placement="bottom"> + <PopoverHeader>{repository.name} templates</PopoverHeader> + <PopoverBody> + <p className="mb-1"> + <span className="fw-bold">Repository</span>:  + <TemplateRepositoryLink url={repository.url} /> + </p> + <p className="mb-0"> + <span className="fw-bold">Reference</span>: {repository.ref} + </p> + </PopoverBody> + </UncontrolledPopover> + </Row> + ); + + return ( + <div> + {title} + <Row className="row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5">{elements}</Row> + </div> + ); +} + class Variables extends Component { render() { const { input, handlers } = this.props; @@ -947,13 +1072,18 @@ class Create extends Component { const createLink = ( <DropdownItem onClick={this.toggle}><FontAwesomeIcon icon={faLink} /> Create link</DropdownItem> ); + const templateDetails = input.template ? + "based on " + (templates.all.find(t => t.id === input.template).name) : + ""; + return ( <Fragment> {alert} <ButtonWithMenu color="primary" default={createProject} disabled={disabled} direction="up"> {createLink} </ButtonWithMenu> - {loading && (<FormText color="primary">{loading}</FormText>)} + {templateDetails && (<FormText className="ms-2" color="primary">{templateDetails}</FormText>)} + {loading && (<FormText className="d-block" color="primary">{loading}</FormText>)} <ShareLinkModal show={this.state.showModal} toggle={this.toggle} @@ -972,7 +1102,13 @@ function ShareLinkModal(props) { const { userTemplates } = props.meta; const defaultObj = { - title: false, namespace: false, visibility: false, templateRepo: false, template: false, variables: false + title: false, + description: false, + namespace: false, + visibility: false, + templateRepo: false, + template: false, + variables: false }; const [available, setAvailable] = useState(defaultObj); @@ -993,6 +1129,7 @@ function ShareLinkModal(props) { setAvailable({ title: input.title ? true : false, + description: input.description ? true : false, namespace: true, visibility: true, templateRepo: input.userRepo && userTemplates.fetched && userTemplates.url && userTemplates.ref ? true : false, @@ -1005,6 +1142,7 @@ function ShareLinkModal(props) { useEffect(() => { setInclude({ title: available.title, + description: available.description, namespace: false, visibility: false, templateRepo: available.templateRepo, @@ -1018,6 +1156,8 @@ function ShareLinkModal(props) { let dataObject = {}; if (include.title) dataObject.title = input.title; + if (include.description) + dataObject.description = input.description; if (include.namespace) dataObject.namespace = input.namespace; if (include.visibility) diff --git a/client/src/project/new/ProjectNew.state.js b/client/src/project/new/ProjectNew.state.js index fc31dd8c47..23cdbf810d 100644 --- a/client/src/project/new/ProjectNew.state.js +++ b/client/src/project/new/ProjectNew.state.js @@ -186,6 +186,10 @@ class NewProjectCoordinator { this.setProperty("title", data.title); newInput.title = data.title; } + if (data.description) { + this.setProperty("description", data.description); + newInput.description = data.description; + } if (data.namespace) { // Check if the namespace is available const namespaces = this.projectsModel.get("namespaces.list"); @@ -428,7 +432,8 @@ class NewProjectCoordinator { id: `${source.name}/${template.folder}`, name: template.name, description: template.description, - variables: template.variables + variables: template.variables, + icon: template.icon }); } } @@ -526,7 +531,8 @@ class NewProjectCoordinator { let newProjectData = { project_repository: repositoryUrl, project_namespace: input.namespace, - project_name: input.title + project_name: input.title, + project_description: input.description }; // add template details @@ -567,9 +573,10 @@ class NewProjectCoordinator { } modelUpdates.meta.creation.created = true; modelUpdates.meta.creation.newName = projectResult.result.name; + modelUpdates.meta.creation.newNameSlug = projectResult.result.slug; modelUpdates.meta.creation.newNamespace = projectResult.result.namespace; modelUpdates.meta.creation.newUrl = projectResult.result.url; - const slug = `${projectResult.result.namespace}/${projectResult.result.name}`; + const slug = `${projectResult.result.namespace}/${projectResult.result.slug}`; // update project details like visibility and name modelUpdates.meta.creation.projectError = ""; @@ -683,15 +690,15 @@ class NewProjectCoordinator { // check warnings (temporary problems) let warnings = {}; if (projects && projects.namespaces.fetching) - warnings["namespace"] = "Fetching namespaces."; + warnings["namespace"] = "Fetching namespaces..."; if (meta.namespace.fetching) - warnings["visibility"] = "Verifying visibility constraints."; + warnings["visibility"] = "Verifying visibility constraints..."; if (templates.fetching) - warnings["template"] = "Fetching templates."; + warnings["template"] = "Fetching templates..."; else if (!templates.fetched) - warnings["template"] = "Must get the templates first."; + warnings["template"] = "Must fetch the templates first."; // check title errors (requires user intervention) let errors = {}; diff --git a/client/src/project/new/templatePlaceholder.svg b/client/src/project/new/templatePlaceholder.svg new file mode 100644 index 0000000000..84748563be --- /dev/null +++ b/client/src/project/new/templatePlaceholder.svg @@ -0,0 +1,235 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="svg2" + xml:space="preserve" + width="214.2" + height="214.28" + viewBox="0 0 214.2 214.28" + sodipodi:docname="RENKU_Symbole_CMJN_GREEN_FUCHSIA_PROD.eps"><metadata + id="metadata8"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs + id="defs6" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="640" + inkscape:window-height="480" + id="namedview4" /><g + id="g10" + inkscape:groupmode="layer" + inkscape:label="ink_ext_XXXXXX" + transform="matrix(1.3333333,0,0,-1.3333333,0,214.28)"><g + id="g12" + transform="scale(0.1)"><path + d="m 1112.2,1298.06 c 0,-34.14 -27.67,-61.81 -61.82,-61.81 -34.14,0 -61.814,27.67 -61.814,61.81 0,34.14 27.674,61.81 61.814,61.81 34.15,0 61.82,-27.67 61.82,-61.81" + style="fill:#f24ea0;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path14" /><path + d="m 1359.34,1050.81 c 0,-34.14 -27.68,-61.814 -61.81,-61.814 -34.14,0 -61.81,27.674 -61.81,61.814 0,34.14 27.67,61.81 61.81,61.81 34.13,0 61.81,-27.67 61.81,-61.81" + style="fill:#f24ea0;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path16" /><path + d="m 988.629,927.184 c 0,-34.137 -27.68,-61.809 -61.816,-61.809 -34.133,0 -61.809,27.672 -61.809,61.809 0,34.136 27.676,61.812 61.809,61.812 34.136,0 61.816,-27.676 61.816,-61.812" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path18" /><path + d="m 741.48,1421.68 c 0,-34.13 -27.667,-61.81 -61.808,-61.81 -34.141,0 -61.817,27.68 -61.817,61.81 0,34.14 27.676,61.82 61.817,61.82 34.141,0 61.808,-27.68 61.808,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path20" /><path + d="m 247.199,1298.06 c 0,-34.14 -27.672,-61.81 -61.812,-61.81 -34.145,0 -61.821,27.67 -61.821,61.81 0,34.14 27.676,61.81 61.821,61.81 34.14,0 61.812,-27.67 61.812,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path22" /><path + d="m 741.48,1298.06 c 0,-34.14 -27.667,-61.81 -61.808,-61.81 -34.141,0 -61.817,27.67 -61.817,61.81 0,34.14 27.676,61.81 61.817,61.81 34.141,0 61.808,-27.67 61.808,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path24" /><path + d="m 247.199,1174.43 c 0,-34.13 -27.672,-61.81 -61.812,-61.81 -34.145,0 -61.821,27.68 -61.821,61.81 0,34.14 27.676,61.82 61.821,61.82 34.14,0 61.812,-27.68 61.812,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path26" /><path + d="m 741.48,1174.43 c 0,-34.13 -27.667,-61.81 -61.808,-61.81 -34.141,0 -61.817,27.68 -61.817,61.81 0,34.14 27.676,61.82 61.817,61.82 34.141,0 61.808,-27.68 61.808,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path28" /><path + d="m 247.199,1050.81 c 0,-34.14 -27.672,-61.814 -61.812,-61.814 -34.145,0 -61.821,27.674 -61.821,61.814 0,34.14 27.676,61.81 61.821,61.81 34.14,0 61.812,-27.67 61.812,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path30" /><path + d="m 370.766,1050.81 c 0,-34.14 -27.676,-61.814 -61.805,-61.814 -34.145,0 -61.816,27.674 -61.816,61.814 0,34.14 27.671,61.81 61.816,61.81 34.129,0 61.805,-27.67 61.805,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path32" /><path + d="m 494.34,1050.81 c 0,-34.14 -27.676,-61.814 -61.817,-61.814 -34.128,0 -61.804,27.674 -61.804,61.814 0,34.14 27.676,61.81 61.804,61.81 34.141,0 61.817,-27.67 61.817,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path34" /><path + d="m 741.48,1050.81 c 0,-34.14 -27.667,-61.814 -61.808,-61.814 -34.141,0 -61.817,27.674 -61.817,61.814 0,34.14 27.676,61.81 61.817,61.81 34.141,0 61.808,-27.67 61.808,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path36" /><path + d="m 865.055,1050.81 c 0,-34.14 -27.676,-61.814 -61.809,-61.814 -34.141,0 -61.816,27.674 -61.816,61.814 0,34.14 27.675,61.81 61.816,61.81 34.133,0 61.809,-27.67 61.809,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path38" /><path + d="m 988.629,1050.81 c 0,-34.14 -27.68,-61.814 -61.816,-61.814 -34.133,0 -61.809,27.674 -61.809,61.814 0,34.14 27.676,61.81 61.809,61.81 34.136,0 61.816,-27.67 61.816,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path40" /><path + d="m 1112.2,1050.81 c 0,-34.14 -27.67,-61.814 -61.82,-61.814 -34.14,0 -61.814,27.674 -61.814,61.814 0,34.14 27.674,61.81 61.814,61.81 34.15,0 61.82,-27.67 61.82,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path42" /><path + d="m 494.34,927.184 c 0,-34.137 -27.676,-61.809 -61.817,-61.809 -34.128,0 -61.804,27.672 -61.804,61.809 0,34.136 27.676,61.812 61.804,61.812 34.141,0 61.817,-27.676 61.817,-61.812" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path44" /><path + d="m 865.055,927.184 c 0,-34.137 -27.676,-61.809 -61.809,-61.809 -34.141,0 -61.816,27.672 -61.816,61.809 0,34.136 27.675,61.812 61.816,61.812 34.133,0 61.809,-27.676 61.809,-61.812" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path46" /><path + d="m 1112.2,927.184 c 0,-34.137 -27.67,-61.809 -61.82,-61.809 -34.14,0 -61.814,27.672 -61.814,61.809 0,34.136 27.674,61.812 61.814,61.812 34.15,0 61.82,-27.676 61.82,-61.812" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path48" /><path + d="m 494.34,803.559 c 0,-34.141 -27.676,-61.809 -61.817,-61.809 -34.128,0 -61.804,27.668 -61.804,61.809 0,34.14 27.676,61.816 61.804,61.816 34.141,0 61.817,-27.676 61.817,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path50" /><path + d="m 741.48,803.559 c 0,-34.141 -27.667,-61.809 -61.808,-61.809 -34.141,0 -61.817,27.668 -61.817,61.809 0,34.14 27.676,61.816 61.817,61.816 34.141,0 61.808,-27.676 61.808,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path52" /><path + d="m 865.055,803.559 c 0,-34.141 -27.676,-61.809 -61.809,-61.809 -34.141,0 -61.816,27.668 -61.816,61.809 0,34.14 27.675,61.816 61.816,61.816 34.133,0 61.809,-27.676 61.809,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path54" /><path + d="m 988.629,803.559 c 0,-34.141 -27.68,-61.809 -61.816,-61.809 -34.133,0 -61.809,27.668 -61.809,61.809 0,34.14 27.676,61.816 61.809,61.816 34.136,0 61.816,-27.676 61.816,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path56" /><path + d="m 1112.2,803.559 c 0,-34.141 -27.67,-61.809 -61.82,-61.809 -34.14,0 -61.814,27.668 -61.814,61.809 0,34.14 27.674,61.816 61.814,61.816 34.15,0 61.82,-27.676 61.82,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path58" /><path + d="m 494.34,679.934 c 0,-34.141 -27.676,-61.817 -61.817,-61.817 -34.128,0 -61.804,27.676 -61.804,61.817 0,34.14 27.676,61.816 61.804,61.816 34.141,0 61.817,-27.676 61.817,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path60" /><path + d="m 741.48,679.934 c 0,-34.141 -27.667,-61.817 -61.808,-61.817 -34.141,0 -61.817,27.676 -61.817,61.817 0,34.14 27.676,61.816 61.817,61.816 34.141,0 61.808,-27.676 61.808,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path62" /><path + d="m 865.055,679.934 c 0,-34.141 -27.676,-61.817 -61.809,-61.817 -34.141,0 -61.816,27.676 -61.816,61.817 0,34.14 27.675,61.816 61.816,61.816 34.133,0 61.809,-27.676 61.809,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path64" /><path + d="m 1112.2,679.934 c 0,-34.141 -27.67,-61.817 -61.82,-61.817 -34.14,0 -61.814,27.676 -61.814,61.817 0,34.14 27.674,61.816 61.814,61.816 34.15,0 61.82,-27.676 61.82,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path66" /><path + d="m 1112.2,556.309 c 0,-34.137 -27.67,-61.821 -61.82,-61.821 -34.14,0 -61.814,27.684 -61.814,61.821 0,34.14 27.674,61.808 61.814,61.808 34.15,0 61.82,-27.668 61.82,-61.808" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path68" /><path + d="m 1235.77,556.309 c 0,-34.137 -27.67,-61.821 -61.81,-61.821 -34.14,0 -61.82,27.684 -61.82,61.821 0,34.14 27.68,61.808 61.82,61.808 34.14,0 61.81,-27.668 61.81,-61.808" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path70" /><path + d="m 1359.34,556.309 c 0,-34.137 -27.68,-61.821 -61.81,-61.821 -34.14,0 -61.81,27.684 -61.81,61.821 0,34.14 27.67,61.808 61.81,61.808 34.13,0 61.81,-27.668 61.81,-61.808" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path72" /><path + d="m 1482.91,556.309 c 0,-34.137 -27.67,-61.821 -61.81,-61.821 -34.13,0 -61.81,27.684 -61.81,61.821 0,34.14 27.68,61.808 61.81,61.808 34.14,0 61.81,-27.668 61.81,-61.808" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path74" /><path + d="m 741.48,432.691 c 0,-34.14 -27.667,-61.82 -61.808,-61.82 -34.141,0 -61.817,27.68 -61.817,61.82 0,34.129 27.676,61.797 61.817,61.797 34.141,0 61.808,-27.668 61.808,-61.797" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path76" /><path + d="m 865.055,432.691 c 0,-34.14 -27.676,-61.82 -61.809,-61.82 -34.141,0 -61.816,27.68 -61.816,61.82 0,34.129 27.675,61.797 61.816,61.797 34.133,0 61.809,-27.668 61.809,-61.797" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path78" /><path + d="m 370.766,309.059 c 0,-34.137 -27.676,-61.809 -61.805,-61.809 -34.145,0 -61.816,27.672 -61.816,61.809 0,34.14 27.671,61.812 61.816,61.812 34.129,0 61.805,-27.672 61.805,-61.812" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path80" /><path + d="m 494.34,309.059 c 0,-34.137 -27.676,-61.809 -61.817,-61.809 -34.128,0 -61.804,27.672 -61.804,61.809 0,34.14 27.676,61.812 61.804,61.812 34.141,0 61.817,-27.672 61.817,-61.812" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path82" /><path + d="m 617.914,309.059 c 0,-34.137 -27.676,-61.809 -61.816,-61.809 -34.141,0 -61.805,27.672 -61.805,61.809 0,34.14 27.664,61.812 61.805,61.812 34.14,0 61.816,-27.672 61.816,-61.812" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path84" /><path + d="m 741.48,309.059 c 0,-34.137 -27.667,-61.809 -61.808,-61.809 -34.141,0 -61.817,27.672 -61.817,61.809 0,34.14 27.676,61.812 61.817,61.812 34.141,0 61.808,-27.672 61.808,-61.812" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path86" /><path + d="m 865.055,309.059 c 0,-34.137 -27.676,-61.809 -61.809,-61.809 -34.141,0 -61.816,27.672 -61.816,61.809 0,34.14 27.675,61.812 61.816,61.812 34.133,0 61.809,-27.672 61.809,-61.812" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path88" /><path + d="m 988.629,309.059 c 0,-34.137 -27.68,-61.809 -61.816,-61.809 -34.133,0 -61.809,27.672 -61.809,61.809 0,34.14 27.676,61.812 61.809,61.812 34.136,0 61.816,-27.672 61.816,-61.812" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path90" /><path + d="m 370.766,185.43 c 0,-34.129 -27.676,-61.801 -61.805,-61.801 -34.145,0 -61.816,27.672 -61.816,61.801 0,34.14 27.671,61.82 61.816,61.82 34.129,0 61.805,-27.68 61.805,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path92" /><path + d="m 741.48,1545.3 c 0,-34.13 -27.667,-61.8 -61.808,-61.8 -34.141,0 -61.817,27.67 -61.817,61.8 0,34.15 27.676,61.82 61.817,61.82 34.141,0 61.808,-27.67 61.808,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path94" /><path + d="m 247.199,1421.68 c 0,-34.13 -27.672,-61.81 -61.812,-61.81 -34.145,0 -61.821,27.68 -61.821,61.81 0,34.14 27.676,61.82 61.821,61.82 34.14,0 61.812,-27.68 61.812,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path96" /><path + d="m 617.914,1421.68 c 0,-34.13 -27.676,-61.81 -61.816,-61.81 -34.141,0 -61.805,27.68 -61.805,61.81 0,34.14 27.664,61.82 61.805,61.82 34.14,0 61.816,-27.68 61.816,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path98" /><path + d="m 865.055,1421.68 c 0,-34.13 -27.676,-61.81 -61.809,-61.81 -34.141,0 -61.816,27.68 -61.816,61.81 0,34.14 27.675,61.82 61.816,61.82 34.133,0 61.809,-27.68 61.809,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path100" /><path + d="m 123.629,1298.06 c 0,-34.14 -27.6759,-61.81 -61.8165,-61.81 -34.1406,0 -61.80859375,27.67 -61.80859375,61.81 0,34.14 27.66799375,61.81 61.80859375,61.81 34.1406,0 61.8165,-27.67 61.8165,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path102" /><path + d="m 370.766,1298.06 c 0,-34.14 -27.676,-61.81 -61.805,-61.81 -34.145,0 -61.816,27.67 -61.816,61.81 0,34.14 27.671,61.81 61.816,61.81 34.129,0 61.805,-27.67 61.805,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path104" /><path + d="m 1235.77,1298.06 c 0,-34.14 -27.67,-61.81 -61.81,-61.81 -34.14,0 -61.82,27.67 -61.82,61.81 0,34.14 27.68,61.81 61.82,61.81 34.14,0 61.81,-27.67 61.81,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path106" /><path + d="m 988.629,1174.43 c 0,-34.13 -27.68,-61.81 -61.816,-61.81 -34.133,0 -61.809,27.68 -61.809,61.81 0,34.14 27.676,61.82 61.809,61.82 34.136,0 61.816,-27.68 61.816,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path108" /><path + d="m 1112.2,1174.43 c 0,-34.13 -27.67,-61.81 -61.82,-61.81 -34.14,0 -61.814,27.68 -61.814,61.81 0,34.14 27.674,61.82 61.814,61.82 34.15,0 61.82,-27.68 61.82,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path110" /><path + d="m 1235.77,1174.43 c 0,-34.13 -27.67,-61.81 -61.81,-61.81 -34.14,0 -61.82,27.68 -61.82,61.81 0,34.14 27.68,61.82 61.82,61.82 34.14,0 61.81,-27.68 61.81,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path112" /><path + d="m 1359.34,1174.43 c 0,-34.13 -27.68,-61.81 -61.81,-61.81 -34.14,0 -61.81,27.68 -61.81,61.81 0,34.14 27.67,61.82 61.81,61.82 34.13,0 61.81,-27.68 61.81,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path114" /><path + d="m 1235.77,1050.81 c 0,-34.14 -27.67,-61.814 -61.81,-61.814 -34.14,0 -61.82,27.674 -61.82,61.814 0,34.14 27.68,61.81 61.82,61.81 34.14,0 61.81,-27.67 61.81,-61.81" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path116" /><path + d="m 741.48,927.184 c 0,-34.137 -27.667,-61.809 -61.808,-61.809 -34.141,0 -61.817,27.672 -61.817,61.809 0,34.136 27.676,61.812 61.817,61.812 34.141,0 61.808,-27.676 61.808,-61.812" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path118" /><path + d="m 1235.77,927.184 c 0,-34.137 -27.67,-61.809 -61.81,-61.809 -34.14,0 -61.82,27.672 -61.82,61.809 0,34.136 27.68,61.812 61.82,61.812 34.14,0 61.81,-27.676 61.81,-61.812" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path120" /><path + d="m 617.914,803.559 c 0,-34.141 -27.676,-61.809 -61.816,-61.809 -34.141,0 -61.805,27.668 -61.805,61.809 0,34.14 27.664,61.816 61.805,61.816 34.14,0 61.816,-27.676 61.816,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path122" /><path + d="m 617.914,679.934 c 0,-34.141 -27.676,-61.817 -61.816,-61.817 -34.141,0 -61.805,27.676 -61.805,61.817 0,34.14 27.664,61.816 61.805,61.816 34.14,0 61.816,-27.676 61.816,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path124" /><path + d="m 988.629,679.934 c 0,-34.141 -27.68,-61.817 -61.816,-61.817 -34.133,0 -61.809,27.676 -61.809,61.817 0,34.14 27.676,61.816 61.809,61.816 34.136,0 61.816,-27.676 61.816,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path126" /><path + d="m 1482.91,679.934 c 0,-34.141 -27.67,-61.817 -61.81,-61.817 -34.13,0 -61.81,27.676 -61.81,61.817 0,34.14 27.68,61.816 61.81,61.816 34.14,0 61.81,-27.676 61.81,-61.816" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path128" /><path + d="m 617.914,556.309 c 0,-34.137 -27.676,-61.821 -61.816,-61.821 -34.141,0 -61.805,27.684 -61.805,61.821 0,34.14 27.664,61.808 61.805,61.808 34.14,0 61.816,-27.668 61.816,-61.808" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path130" /><path + d="m 741.48,556.309 c 0,-34.137 -27.667,-61.821 -61.808,-61.821 -34.141,0 -61.817,27.684 -61.817,61.821 0,34.14 27.676,61.808 61.817,61.808 34.141,0 61.808,-27.668 61.808,-61.808" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path132" /><path + d="m 865.055,556.309 c 0,-34.137 -27.676,-61.821 -61.809,-61.821 -34.141,0 -61.816,27.684 -61.816,61.821 0,34.14 27.675,61.808 61.816,61.808 34.133,0 61.809,-27.668 61.809,-61.808" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path134" /><path + d="m 1606.49,556.309 c 0,-34.137 -27.68,-61.821 -61.82,-61.821 -34.14,0 -61.81,27.684 -61.81,61.821 0,34.14 27.67,61.808 61.81,61.808 34.14,0 61.82,-27.668 61.82,-61.808" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path136" /><path + d="m 1482.91,432.691 c 0,-34.14 -27.67,-61.82 -61.81,-61.82 -34.13,0 -61.81,27.68 -61.81,61.82 0,34.129 27.68,61.797 61.81,61.797 34.14,0 61.81,-27.668 61.81,-61.797" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path138" /><path + d="m 247.199,185.43 c 0,-34.129 -27.672,-61.801 -61.812,-61.801 -34.145,0 -61.821,27.672 -61.821,61.801 0,34.14 27.676,61.82 61.821,61.82 34.14,0 61.812,-27.68 61.812,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path140" /><path + d="m 494.34,185.43 c 0,-34.129 -27.676,-61.801 -61.817,-61.801 -34.128,0 -61.804,27.672 -61.804,61.801 0,34.14 27.676,61.82 61.804,61.82 34.141,0 61.817,-27.68 61.817,-61.82" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path142" /><path + d="M 370.766,61.8086 C 370.766,27.6719 343.09,0 308.961,0 c -34.145,0 -61.816,27.6719 -61.816,61.8086 0,34.1406 27.671,61.8204 61.816,61.8204 34.129,0 61.805,-27.6798 61.805,-61.8204" + style="fill:#00a878;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path144" /></g></g></svg>