Coder Social home page Coder Social logo

stephenott / camunda-formio-plugin Goto Github PK

View Code? Open in Web Editor NEW
62.0 9.0 24.0 3.78 MB

Integration for Drag and Drop Formio Form Builder and Renderer with Camunda Tasklist App

License: MIT License

Kotlin 53.06% JavaScript 0.60% CSS 0.26% HTML 46.08%
camunda formio

camunda-formio-plugin's Introduction

Camunda Formio Plugin

Provides client-side and server-side integration for using Camunda Embedded Forms with Formio Forms.

What does it do?

Allows you to configure start-event forms and user-task forms with a formkey that will load formio forms in Camunda Tasklist webapp.

How do I set it up?!

Configuring Formio for a Start Form:

Start Form

Configuring Formio for User Tasks:

User Task Form

If you want to add server side validation, you add a Validation Constraint with the name formio. See the Server Validation section for more details.

Installation

Camunda SpringBoot Deployment

See the springboot folder for a example of the deployment

Typical Camunda Deployment

See the docker folder for an example of the deployment using the Camunda Tomcat distribution

Building Forms:

Local Builder: Deploy Webapp and access a local builder at: http://..../forms/builder.html

Hosted Builder: https://formio.github.io/formio.js/app/builder

Configure the BPMN

Example of Configuring a Start Form:

embedded:/forms/formio.html?deployment=MyStartForm.json&var=submission&transient=true

In this example, we use the deployment parameter to direct the use of the MyStartForm.json form schema which is found in the BPMN Deployment resources. We use the var parameter to direct the name of the process variable that will be created to store the form submission. We use the transient parameter to direct the process variable holding the form submission get created as a transient variable

start form configuration

<bpmn:startEvent id="Event_0emfvgy"
                 camunda:formKey="embedded:/forms/formio.html?deployment=MyStartForm.json&#38;var=submission&#38;transient=true">
    <bpmn:outgoing>Flow_1ax32ut</bpmn:outgoing>
</bpmn:startEvent>

Example of Configuring a User Task:

`embedded:/forms/formio.html?deployment=MyUT1.json&var=subWithServerValidation

start form configuration

<bpmn:userTask id="Activity_1xq7c62" name="Typical Form with Server Validation"
               camunda:formKey="embedded:/forms/formio.html?deployment=MyUT1.json&#38;var=subWithServerValidation">
    <bpmn:extensionElements>
        <camunda:formData>
            <camunda:formField id="FormField_0t7u03d" type="string">
                <camunda:validation>
                    <camunda:constraint name="formio"/>
                </camunda:validation>
            </camunda:formField>
        </camunda:formData>
    </bpmn:extensionElements>
    <bpmn:incoming>Flow_0069xxd</bpmn:incoming>
    <bpmn:outgoing>Flow_18xtnen</bpmn:outgoing>
</bpmn:userTask>

Configuration Options:

The following are the parameters that can be passed in the formKey:

Parameter Required? Default Description
deployment= Yes or use path= - The .json file name in the deployment created through the API.
path= Yes or use deployment= - The file system path to the .json file. Must start with a /
transient= No false If true, then variable will be submitted as Transient, and thus not saved to database. Use a Script Task or listener in the transaction to post-process the submission into the desired variable.
var= No if start form then "startForm_submission", if user task then "[taskId]_submission" Define a custom variable name for the form submission. Will be submitted as a Process Variable.

Examples:

  1. embedded:/forms/formio.html?deployment=MyUT1.json?transient=true&var=myCustomSubmission
  2. embedded:/forms/formio.html?deployment=MyUT1.json?var=UT1-Submission
  3. embedded:/forms/formio.html?path=/forms/MyStartForm.json (where the MyStartForm.json was placed in the src/main/webapp/forms folder. Make sure your path starts with a /)

Configuration through Camunda Extension Properties

If you enable the FormioParseListenerProcessEnginePlugin, you can configure through Extension Properties rather than manually creating the formKey:

extension props usage

The same configuration options used in the formKey are used in the extension properties. Each extension property has a prefix of formio_.

For Server Validation you add a name: formio_validation, value: true.

Submission Storage

When a successful submission occurs, a json variable will be created as a Process Instance Variable.

Use Camunda SPIN library to access the json variable properties.

Example in a gateway expression: ${someSubmission.prop('data').prop('someFieldKey').value()}

Resolving User Task taskId to more meaningful values

Very often the taskID (which is typically a UUID) will not be very meaningful. If you require more meaningful variable names consider using the var parameter to set a custom variable name or use the Activity ID (the id property when you are in the Modeler on a user task activity).

Trigger 'BPMN Errors'

Support is provided to trigger interrupting BPMN Errors:

Trigger a formio event with the name bpmn-error (typically with a button: set the button event to bpmn-error)

The default error code is default. To set a custom error code, create a variable in the formio submission with the key _errorCode. Typical use cases are to use a text component or a hidden component.

No error message is submitted by default. To set a custom error message, create a variable in the formio submission with the key _errorMessage. Typical use cases are to use a text component or a hidden component.

The submission variable created through the bpmn-error cannot be validated using the formio server validator: this is a limitation in Camunda

Trigger 'BPMN Escalations'

Support is provided to trigger interrupting and non-interrupting BPMN Escalations. Note that Camunda's form API does not make a distinction between interrupting and non-interrupting escalation events and therefore some best practices are implemented:

The escalation code is default. To set a custom escalation code, create a variable in the formio submission with the key _escalationCode. Typical use cases are to use a text component or a hidden component.

The submission variable created through the bpmn-escalation cannot be validated using the formio server validator: this is a limitation in Camunda

Interrupting BPMN Escalations

To trigger BPMN Escalation that is designed to be used with a interrupting BPMN Escalation boundary event:

Trigger a formio event with the name bpmn-escalation (typically with a button: set the button event to bpmn-escalaton)

Non-Interrupting BPMN Escalations

To trigger BPMN Escalation that is designed to be used with a non-interrupting BPMN Escalation boundary event:

Trigger a formio event with the name bpmn-escalation-noninterrupt (typically with a button: set the button event to bpmn-escalaton-noninterrupt)

A non-interrupting escalation means the user task will remain in the task list, and the submission variable name will be given a suffix of _escalation. The suffix is used to ensure if/when the user task is normally completed, the submission variable created through the user task completion does not overwrite the submission variable created through escalation.

Deploying your Forms

REST API Form Deployment

Forms can be deployed through the REST API. The form JSON must be part of the same deployment as the BPMN.

If changes need to be made to the form, you must deploy a new .json file along with the BPMN.

An example of using Postman to deploy:

Deployment in Postman

When using REST API Form deployments, use the deployment parameter in the form key such as: embedded:/forms/formio.html?deployment=MyUT1.json

File System Forms Deployment (or other URLs)

Forms can be deployed on the file system and made available to the web application. The common way to do this is through src/main/webapp/forms (or whatever folder you like within webapp).

If you want to make changes to the JSON, you can modify the JSON file without having to make a new BPMN deployment.

If you do not want to use the file system, you can deploy to another URL within the same domain as Camunda Webapps. Then you can set your formKey to something like: embedded:/forms/formio.html?deployment=http://example.com/forms/MyUT1.json

The benefit of having your form/JSON outside of the BPMN deployment is you are not required to redeploy the BPMN each time you make changes to the form, but in many use cases you will want to tie your BPMN and forms together within the same deployment for versioning purposes.

Example Submission

Cockpit

{
 "data": {
   "firstName": "SomeFirstName",
   "lastName": "SomeLastName",
   "email": "",
   "phoneNumber": {
     "value": "",
     "maskName": "US"
   },
   "select": [],
   "address": {},
   "dueDate": "2020-08-18T12:00:00-04:00",
   "birthdate": "00/00/0000",
   "submit": false
 },
 "metadata": {
   "timezone": "America/Montreal",
   "offset": -240,
   "origin": "http://localhost:8080",
   "referrer": "http://localhost:8080/camunda/app/welcome/default/",
   "browserName": "Netscape",
   "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15",
   "pathName": "/camunda/app/tasklist/default/",
   "onLine": true
 },
 "state": "submitted"
}

Variable Fetching in Formio

Formio will fetch variables based on configurations in the component configuration.

Under the API tab of a component create a custom property with the following format:

key: fetchVariable value: variableName (recommended not to use spaces in variable names)

Once you get your variable, use the Default Value Population feature to populate your field with the data returned from the variable fetch.

Default Value Population

Make sure to fetch the variables using the fetchVariable configuration.

Simple Configuration

The simple configuration provides a rapid setup option allowing most common use case access to variables.

In the form component's API tab, set a custom property with key: camVariableName and the value being the dot notation path.

Example:

  1. Given a simple camunda string variable named "firstName", the key-value configuration would be:

    camVariableName: firstName

  2. Given a previous formio form submission which is saved as a Camunda Json variable, the key-value configuration would be:

    camVariableName: somePreviousSubmission.data.firstName

If the variable is type Json and you would like to print out the json into the form field, add a property in the API tab with the configuration:

Key:stringify

Value: true (If you are manipulating the raw json, this should be an equivalent of "true")

Advanced Configuration

The advanced configuration provides javascript access to the variables, allowing complex configurations such as variable manipulation, merging, trim, etc.

Use the Custom Default Value Javascript feature in Formio to parse the returned variables.

Variables get stored in $scope.camForm.formioVariables

In the Custom Default Value configuration you can use the following to access the object of variables:

let variables = angular.element('#task-form-formio').scope().camForm.formioVariables

Variables names are the keys.

If the variable is type Json it is automatically available as a JS Object.

Example

Set the default value of a Text field "First Name" with the firstName property that was submitted in the Start Form (a formio form submission)

In the First Name field's Custom Default Value configuration use the following:

let variables = angular.element('#task-form-formio').scope().camForm.formioVariables

value = variables.postProcessed_submission.data.firstName

The code angular.element('#task-form-formio').scope() is re-capturing the Camunda task form angular scope from the <form id="task-form-formio> element.

Formio based submissions place the form submission data inside of the data object.

Common use case would be to set the First Name field as read-only if it is for display purposes.

  1. Configure the Component:

    Build1

  2. Go to the Data tab:

    Build2

  3. Scroll down to Custom Default Value:

    Build3

  4. Add your JS logic for selecting your default value:

    Build4

  5. Go to the API tab:

    Build5

  6. Create a Custom property with key fetchVariable, and the value of the variable you want to work within your JS in step 4.

    Build6

Server Validation (Validating submissions against the schema on the server)

From submissions can optionally be enabled with server-side validation by the Formio server-side validation.

To enable server-side validation of a start-form or user-task:

  1. add a "validation constraint" in the form fields configuration.

  2. Set any field type, and create a constraint with the key/name "formio":

    server config

  3. Deploy the form-validation-server

  4. Configure the Plugin FormioFormFieldValidationProcessEnginePlugin.class. The plugin has the following parameters:

    1. validationUrl : The validation url to send submissions to. Defaults to localhost:8081/validate.
    2. validationTimeout : The milliseconds to wait before timeout of the Validation Url HTTP request. Defaults to 10000 10 seconds.
    3. validationHandler : The bean instance that will execute the Formio Validation. Defaults to an instance of SimpleFormioValidationHandler.class. Override this configuration if you have special validation handling requirements.

Subforms / Nested Forms

You can nest forms using the container component. The container component is used to gather form values into a nested JSON object.

Subform usage

  1. Add a container to your form
  2. In the api properties tab set a component property of:
    1. Key: subform value: deployment=mySubForm.json or path=/my/path/mySubForm.json

The subform property value uses the same format as the formkey parameters.

Typical use cases is to set the container to "disabled", then the nested form is read-only and you display the form of a previous submission.

Hide Subform buttons

The submit button will be hidden by default. If you want to hide all buttons on your subform add a additional property to the container:

  • Key: hideButtons value: true (this should be a string value)

Subform Pre-population

If you want to populate the subform with values from a variable, use the camVariableName property. You can access previous form submissions with Key: camVariableName value: myPreviousSubmissionJsonVariable.data. The data object, which is what Formio places all submission data into, will then be populated into the subform.

It is the responsibility of the parent form to provide variable resolution for any fields in the subform. Subforms with form components that have camVariableName or fetchVariable properties will be ignored. This may change in the future.

File Uploads

Use the file component, and use the Base64 storage format. The file will be uploaded as part of the JSON submission to camunda.

You can display you file upload in a formio form by returning the base64 value into a readonly/disabled form (such as using subforms).

Typical use case would be to upload the file as JSON/base64 as part of the form submission, and then handle transitive modifications of the file into other storage formats, and drop the base64 value from the submission / replace with other values pointing to a long term storage format (such as a blob/file storage container)

Get-Form-Variables Command Security Plugin

The plugin GetFormVariablesSecurityProcessEnginePlugin.class provides variable security using Camunda Extension Properties on a User Task.

Plugin full path: com.github.stephenott.camunda.tasks.forms.command.GetFormVariablesSecurityProcessEnginePlugin

The plugin provides two types of variable security:

  1. allowed-variables : a comma separated list of variable names that can be accessed using the endpoint GET /task/{id}/form-variables or the java api (getFormVariables).
  2. restricted-variables : a comma separated list of variable names that cannot be accessed using the endpoint GET /task/{id}/form-variables or the java api (getFormVariables).

allowed-variables is used to control the exact list of variables that can be accessed. Any variables that are not part of this list will be removed from the result. No error will be thrown.

restricted-variables is used to control which variables cannot be accessed. Any variables that are part of this will be removed from the result. No error will be thrown.

Example:

allowed-variables

restricted-variables

camunda-formio-plugin's People

Contributors

stephenott avatar victorhugofranca avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

camunda-formio-plugin's Issues

Form Validation Error 404 Not Found

I have been working on this for weeks off and on and the only place I am able to get the forms to work properly is your demo process.

So in an effort to keep updated, I update to the Release v5 today and when I use the demo to submit the "Typical Form with Server Validation" User Task, I get the following error.

HTTP Status 404 – Not Found
Type Status Report

Message /validate

Description The origin server did not find a current representation for the target resource or is not willing to disclose that one exists.

An error happened while submitting the task form :
Cannot submit task form 5cd9ee12-0c5d-11eb-aca9-0a0027000003: <!doctype html><html lang="en"><head><title>HTTP Status 404 – Not Found</title><style type="text/css">h1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} h2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} h3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} body {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} b {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} p {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;} a {color:black;} a.name {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 404 – Not Found</h1><hr class="line" /><p><b>Type</b> Status Report</p><p><b>Message</b> &#47;validate</p><p><b>Description</b> The origin server did not find a current representation for the target resource or is not willing to disclose that one exists.</p><hr class="line" /><h3>Apache Tomcat/9.0.24</h3></body></html>

Any help would be greatly appreciated..

Demo works but not able to get working in another process

I have been trying for weeks to get this working in one of our custom forms. The Demo works great and I'm using it as a reference.

Here is the current error we are getting when we submit the form in our custom process.

image_2020_10_12T08_37_22_244Z

Here is our setting in the Modeler.

image

I'm still not sure how the var is used. In an attempt to understand better, I searched in the Demo Process XML and the form JSON for "noPersitSubmission" but still puzzled exactly how to use the var parameter, but I'm not sure if this has anything to do with the error or not.

Provide example of doing a file upload in the form

Likely can just use the base64 upload within the form + do post processing to save it as a file variable?

Maybe have some additional logic to detect the file upload component and move that variable into a file variable in the file manager?

Clarification on accessing the variables in forms

Is there an example BPMN file showing how to setup the variables and access them once the form is submitted?

"Under the API tab of a component create a custom property with the following format:"

Also, can you clarify what program you are in when you made this instruction?

Add proper plugin

Add proper plugin to support regular camunda deployments (spring and non-spring)

Add plugin for injection of custom jersey endpoint

features required:

  • Support for Default Engine
  • Support for Named Engine
  • Support for UnAuth usage
  • Support for Authenticated Usage (Authorization Checks based on authz code used for get-deployed-form endpoints)
  • Support for Sub Forms
  • Support for getting user task variables
  • Ensure that all actions are occurring as part of the single parent Transaction / Context... currently they are not well structured for this...

Form.io Create Form Help

Hey @StephenOTT ,

I looked through the documentation and was not able to find much on the actual creation of forms so I thought I would start a thread and help make a better tutorial.

Here are the steps I have done:

  • Create Account on Form.io
  • Create New Form
  • Copy Embed JSON to Local File "Form.json"
  • Paste Form Key "embedded:/forms/formio.html?deployment=Form.json
  • Deploy through Postman
  • Start Process Manually

When I tested the process in the tasklist I did not get any form and I'm not sure where to start testing.

What would you suggest?

<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1hf56xc" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.1.1">
  <bpmn:process id="FormIOTest" isExecutable="true">
    <bpmn:startEvent id="StartEvent_1">
      <bpmn:outgoing>Flow_10lpabf</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:sequenceFlow id="Flow_10lpabf" sourceRef="StartEvent_1" targetRef="Activity_1cwkjt5" />
    <bpmn:userTask id="Activity_1cwkjt5" name="Test Form" camunda:formKey="embedded:/forms/formio.html?deployment=Form.json&#38;var=subWithServerValidation">
      <bpmn:incoming>Flow_10lpabf</bpmn:incoming>
      <bpmn:outgoing>Flow_1n9zxzx</bpmn:outgoing>
    </bpmn:userTask>
    <bpmn:endEvent id="Event_161cw66">
      <bpmn:incoming>Flow_1n9zxzx</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_1n9zxzx" sourceRef="Activity_1cwkjt5" targetRef="Event_161cw66" />
  </bpmn:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="FormIOTest">
      <bpmndi:BPMNEdge id="Flow_1n9zxzx_di" bpmnElement="Flow_1n9zxzx">
        <di:waypoint x="380" y="117" />
        <di:waypoint x="452" y="117" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_10lpabf_di" bpmnElement="Flow_10lpabf">
        <di:waypoint x="215" y="117" />
        <di:waypoint x="280" y="117" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds x="179" y="99" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_09bbdei_di" bpmnElement="Activity_1cwkjt5">
        <dc:Bounds x="280" y="77" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_161cw66_di" bpmnElement="Event_161cw66">
        <dc:Bounds x="452" y="99" width="36" height="36" />
      </bpmndi:BPMNShape>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>

JSON data issue on Hosted Form Builder code

@StephenOTT I've been doing testing and I have the plug-in installed correctly and your "SomeProcess1" demo process working. When I start the process, there is a nice fancy form.

So as a test, I went to the hosted form builder URL and made a small test form and was simply going to replace the code in your MyStartForm.json file with the code of the form I built and re-deploy to see if I could work from your demo.

When I try to start the process, it does not render.

Replacing your original MyStartForm.json works again. Ok, so we have a definite issue.

I opened both my version and your version of the MyStartForm.json file and compared it. Below is the code for my version of the form.

The one things I noticed was my hosted builder code had 5 extra lines of code that were extra data elements at the top;

display form, settings { pdf { id 1ec0f8ee-6685-5d98-a847-26f67b67d6f0, src httpsfiles.form.iopdf5692b91fd1028f01000407e3file1ec0f8ee-6685-5d98-a847-26f67b67d6f0 } },

When I removed these lines and which made the code more in-line with your original format and deploy the code where it displays the simple form.

So I'm not sure if the Hosted Form Builder is possibly on a new/older version or what, but there definately seems like there might be an issue with rending the Hosted Form Builder JSON Code.

Below is the original code from the Hosted Form Builder.

{ display form, settings { pdf { id 1ec0f8ee-6685-5d98-a847-26f67b67d6f0, src httpsfiles.form.iopdf5692b91fd1028f01000407e3file1ec0f8ee-6685-5d98-a847-26f67b67d6f0 } }, components [ { label Name, placeholder Texas, description Please type your name., tooltip Please type your name., tableView true, key name, type textfield, input true }, { type button, label Submit, key submit, disableOnInvalid true, input true, tableView false } ] }

Help with Camunda's Run distro (Docker)

Camunda has a self-contained distro called Run. There is also a docker variant which I'm using.

As I'm a novice to Java, Tomcat, etc, I'm struggling to overlay your instructions onto Camunda Run. In particular, I'm reluctant to tinker with the internal folder as it implies "off limits" :).

Could some instructions to integrate this plugin into Camunda Run (docker variant) be created? Some ideas:

  • craft a child Dockerfile to build a custom image with this plugin already "deployed"
  • use -v [local file system]:[container file system] volume mapping(s) to inject this plugin at container start

I use the latter technique to inject my custom .bpmn files into the /configuration/resources folder. However, I've found that only resources in the same deployment (startupDeployment) can be referenced in form keys (embedded:deployment://). Whereas I'd prefer a once-off deployment of this plugin be accessible from all processes, even those done after startup (e.g. via Modeler).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.