atlas-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From suma...@apache.org
Subject incubator-atlas git commit: ATLAS-163 New Trait UI (Tag) darshankumar89 via sumasai
Date Thu, 08 Oct 2015 16:17:46 GMT
Repository: incubator-atlas
Updated Branches:
  refs/heads/master 69e33ad6c -> 538fa496e


ATLAS-163 New Trait UI (Tag) darshankumar89 via sumasai


Project: http://git-wip-us.apache.org/repos/asf/incubator-atlas/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-atlas/commit/538fa496
Tree: http://git-wip-us.apache.org/repos/asf/incubator-atlas/tree/538fa496
Diff: http://git-wip-us.apache.org/repos/asf/incubator-atlas/diff/538fa496

Branch: refs/heads/master
Commit: 538fa496eed1f76a0dfaab8ee313ea3be51f21d3
Parents: 69e33ad
Author: Suma Shivaprasad <sumasai.shivaprasad@gmail.com>
Authored: Thu Oct 8 21:47:14 2015 +0530
Committer: Suma Shivaprasad <sumasai.shivaprasad@gmail.com>
Committed: Thu Oct 8 21:47:14 2015 +0530

----------------------------------------------------------------------
 dashboard/public/css/common.css                 |   8 +-
 dashboard/public/js/app.js                      |   3 +-
 .../public/modules/home/headerController.js     |   5 +-
 .../modules/tags/tagAttributeDefinition.js      |  33 +++++
 dashboard/public/modules/tags/tagClasses.js     |  76 +++++++++++
 dashboard/public/modules/tags/tagsController.js |  63 +++++++++
 dashboard/public/modules/tags/tagsModule.js     |  21 +++
 dashboard/public/modules/tags/tagsResource.js   |  41 ++++++
 dashboard/public/modules/tags/tagsRoutes.js     |  28 ++++
 dashboard/public/modules/tags/views/add.html    | 134 +++++++++++++++++++
 release-log.txt                                 |   1 +
 11 files changed, 410 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/538fa496/dashboard/public/css/common.css
----------------------------------------------------------------------
diff --git a/dashboard/public/css/common.css b/dashboard/public/css/common.css
index 310e1f9..dbb0a5e 100644
--- a/dashboard/public/css/common.css
+++ b/dashboard/public/css/common.css
@@ -126,4 +126,10 @@ footer.navbar-bottom img {
 .menulink{
     padding-left: 10px;
     padding-right: 10px;
-}
\ No newline at end of file
+}
+
+.btnToggle
+{
+    border: 1px solid #CCCCCC;
+    padding: 5px;
+}

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/538fa496/dashboard/public/js/app.js
----------------------------------------------------------------------
diff --git a/dashboard/public/js/app.js b/dashboard/public/js/app.js
index 9f3bc39..82171c6 100644
--- a/dashboard/public/js/app.js
+++ b/dashboard/public/js/app.js
@@ -26,7 +26,8 @@ angular.module('dgc', ['ngCookies',
     'dgc.home',
     'dgc.about',
     'dgc.search',
-    'dgc.navigation'
+    'dgc.navigation',
+    'dgc.tags'
 ]);
 
 angular.module('dgc.system', ['dgc.system.notification']);

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/538fa496/dashboard/public/modules/home/headerController.js
----------------------------------------------------------------------
diff --git a/dashboard/public/modules/home/headerController.js b/dashboard/public/modules/home/headerController.js
index 43bedb8..6e7df3b 100644
--- a/dashboard/public/modules/home/headerController.js
+++ b/dashboard/public/modules/home/headerController.js
@@ -20,7 +20,10 @@
 
 angular.module('dgc.home').controller('HeaderController', ['$scope', '$modal', function($scope,
$modal) {
 
-    $scope.menu = [];
+    $scope.menu = [{
+        title: "Tags",
+        state: "tags"
+    }];
 
     $scope.isCollapsed = true;
     $scope.isLoggedIn = function() {

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/538fa496/dashboard/public/modules/tags/tagAttributeDefinition.js
----------------------------------------------------------------------
diff --git a/dashboard/public/modules/tags/tagAttributeDefinition.js b/dashboard/public/modules/tags/tagAttributeDefinition.js
new file mode 100644
index 0000000..6b57bd1
--- /dev/null
+++ b/dashboard/public/modules/tags/tagAttributeDefinition.js
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * 'License'); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+angular.module('dgc.tags').service('AttributeDefinition', function Attribute() {
+    this.getModel = function getJson() {
+        return {
+            name: null,
+            dataTypeName: 'string',
+            multiplicity: 'optional',
+            isComposite: false,
+            isUnique: false,
+            isIndexable: true,
+            reverseAttributeName: null
+        };
+    };
+});

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/538fa496/dashboard/public/modules/tags/tagClasses.js
----------------------------------------------------------------------
diff --git a/dashboard/public/modules/tags/tagClasses.js b/dashboard/public/modules/tags/tagClasses.js
new file mode 100644
index 0000000..81ad77c
--- /dev/null
+++ b/dashboard/public/modules/tags/tagClasses.js
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * 'License'); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+angular.module('dgc.tags').factory('TagClasses', ['lodash', function ClassFactory(_) {
+
+    function Tag(props) {
+        return _.merge({
+            superTypes: [],
+            typeName: null,
+            attributeDefinitions: []
+        }, props);
+    }
+
+    var classes = {
+        ENUM: 'org.apache.atlas.typesystem.types.EnumType',
+        TRAIT: 'org.apache.atlas.typesystem.types.TraitType',
+        STRUCT: 'org.apache.atlas.typesystem.types.StructType',
+        CLASS: 'org.apache.atlas.typesystem.types.ClassType'
+    };
+
+    function Class(classId, className) {
+        this.tags = [];
+        this.id = classId;
+        this.name = className;
+
+        this.addTag = function AddTag(props) {
+            var tag = new Tag(_.merge({
+                hierarchicalMetaTypeName: className
+            }, props));
+
+            this.tags.push(tag);
+            return this;
+        };
+
+        this.clearTags = function RemoveTags() {
+            this.tags = [];
+            return this;
+        };
+
+        this.toJson = function CreateJson() {
+            var classTypeKey = (this.id.toLowerCase() + 'Types'),
+                output = {};
+
+            _.forEach(classes, function addTypesArray(className, classKey) {
+                output[classKey.toLowerCase() + 'Types'] = [];
+            });
+
+            output[classTypeKey] = this.tags;
+            return output;
+        };
+    }
+
+    return _.chain(classes)
+        .map(function CreateClass(className, classId) {
+            return new Class(classId, className);
+        })
+        .indexBy('id')
+        .value();
+}]);

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/538fa496/dashboard/public/modules/tags/tagsController.js
----------------------------------------------------------------------
diff --git a/dashboard/public/modules/tags/tagsController.js b/dashboard/public/modules/tags/tagsController.js
new file mode 100755
index 0000000..e548287
--- /dev/null
+++ b/dashboard/public/modules/tags/tagsController.js
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+angular.module('dgc.tags').controller('TagsController', ['$scope', '$resource', '$state',
'$stateParams', 'lodash', 'AttributeDefinition', 'TagClasses', 'TagsResource', 'NotificationService',
+    function($scope, $resource, $state, $stateParams, _, AttributeDefinition, Categories,
TagsResource, NotificationService) {
+        $scope.categoryList = Categories;
+        $scope.category = 'TRAIT';
+        $scope.tagModel = {
+            typeName: null,
+            attributeDefinitions: []
+        };
+
+        $scope.addAttribute = function AddAttribute() {
+            $scope.tagModel.attributeDefinitions.push(AttributeDefinition.getModel());
+        };
+
+        $scope.categoryChange = function CategorySwitched() {
+            $scope.categoryInst = Categories[$scope.category].clearTags();
+        };
+
+        $scope.save = function saveTag(form) {
+            $scope.savedTag = null;
+            if (form.$valid) {
+                $scope.categoryInst = Categories[$scope.category];
+                $scope.categoryInst.clearTags().addTag($scope.tagModel);
+
+                NotificationService.reset();
+                $scope.saving = true;
+
+                TagsResource.save($scope.categoryInst.toJson()).$promise
+                    .then(function TagCreateSuccess() {
+                        NotificationService.info('"' + $scope.tagModel.typeName + '" has
been created', false);
+                        return TagsResource.get({
+                            id: $scope.tagModel.typeName
+                        }).$promise;
+                    }).then(function TagFound(res) {
+                        $scope.savedTag = JSON.parse(res.definition);
+                    }).catch(function TagCreateFailed(error) {
+                        NotificationService.error(error.data.error, false);
+                    }).finally(function() {
+                        $scope.saving = false;
+                    });
+            }
+        };
+    }
+]);

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/538fa496/dashboard/public/modules/tags/tagsModule.js
----------------------------------------------------------------------
diff --git a/dashboard/public/modules/tags/tagsModule.js b/dashboard/public/modules/tags/tagsModule.js
new file mode 100755
index 0000000..6bed79d
--- /dev/null
+++ b/dashboard/public/modules/tags/tagsModule.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+angular.module('dgc.tags', ['dgc.details']);

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/538fa496/dashboard/public/modules/tags/tagsResource.js
----------------------------------------------------------------------
diff --git a/dashboard/public/modules/tags/tagsResource.js b/dashboard/public/modules/tags/tagsResource.js
new file mode 100755
index 0000000..3434034
--- /dev/null
+++ b/dashboard/public/modules/tags/tagsResource.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+angular.module('dgc.tags').factory('TagsResource', ['$resource', function($resource) {
+    return $resource('/api/atlas/types/:id', {}, {
+        query: {
+            method: 'GET',
+            transformResponse: function(data) {
+                var categories = [];
+                if (data) {
+                    angular.forEach(data.results, function(value) {
+                        categories.push({
+                            text: value
+                        });
+                    });
+                }
+                return categories;
+            },
+            responseType: 'json',
+            isArray: true
+        }
+    });
+
+}]);

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/538fa496/dashboard/public/modules/tags/tagsRoutes.js
----------------------------------------------------------------------
diff --git a/dashboard/public/modules/tags/tagsRoutes.js b/dashboard/public/modules/tags/tagsRoutes.js
new file mode 100755
index 0000000..a15c3ba
--- /dev/null
+++ b/dashboard/public/modules/tags/tagsRoutes.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+angular.module('dgc.tags').config(['$stateProvider',
+    function($stateProvider) {
+        $stateProvider.state('tags', {
+            url: '/tags',
+            templateUrl: '/modules/tags/views/add.html'
+        });
+    }
+]);

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/538fa496/dashboard/public/modules/tags/views/add.html
----------------------------------------------------------------------
diff --git a/dashboard/public/modules/tags/views/add.html b/dashboard/public/modules/tags/views/add.html
new file mode 100755
index 0000000..05ddd89
--- /dev/null
+++ b/dashboard/public/modules/tags/views/add.html
@@ -0,0 +1,134 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<div class="container">
+    <div class="appForm" data-ng-controller="TagsController">
+        <h4>Tag Definition</h4>
+
+        <form data-name="tagForm" class="form-horizontal" novalidate role="form">
+            <fieldset data-ng-disabled="saving">
+
+                <div class="form-group hide">
+                    <label for="category" class="col-sm-2 control-label">Category</label>
+
+                    <div class="col-sm-10">
+                        <select class="form-control" id="category" name="category" data-ng-model="category"
data-ng-change="categoryChange()" required>
+                            <option value="{{key}}" data-ng-repeat="(key, value) in categoryList"
ng-selected="{{key===category}}">{{key}}</option>
+                        </select>
+                    </div>
+                </div>
+                <div class="form-group" data-ng-class="{'has-error': tagForm.typeName.$invalid
&& tagForm.typeName.$dirty}">
+                    <label for="typeName" class="col-sm-2 control-label">Tag Name</label>
+
+                    <div class="col-sm-10">
+                        <input type="text" class="form-control" name="typeName" id="typeName"
placeholder="Tag Name" data-ng-model="tagModel.typeName" required/>
+                    </div>
+                </div>
+                <ng-form name="attributeForm">
+                    <div class="form-group" data-ng-class="{'has-error': attributeForm.name.$invalid
&& attributeForm.name.$dirty}"
+                         data-ng-repeat-start="attribute in tagModel.attributeDefinitions">
+                        <label for="attributeId_{{$index}}" class="col-sm-2 control-label">Attribute
name</label>
+
+                        <div class="col-sm-10">
+                          <!--   <div class="input-group"> -->
+                                <input type="text" class="form-control" name="name" id="attributeId_{{$index}}"
placeholder="Attribute name" data-ng-model="attribute.name"
+                                       required/>
+                             <!--    <i class="input-group-addon fa fa-2x" data-ng-class="{'fa-angle-double-down':!attribute.$$show,
'fa-angle-double-up':attribute.$$show}"
+                                   data-ng-click="attribute.$$show=!attribute.$$show"></i>
-->
+                           <!--  </div> -->
+                        </div>
+                    </div>
+                    <div class="form-group" data-ng-class="{'has-error': attributeForm.name.$invalid
&& attributeForm.name.$dirty}" data-ng-show="attribute.$$show">
+                        <label for="attributeDatatype_{{$index}}" class="col-sm-2 control-label">Data
Type Name</label>
+
+                        <div class="col-sm-10">
+                            <input type="text" class="form-control" name="name" id="attributeDatatype_{{$index}}"
placeholder="dataTypeName"
+                                   data-ng-model="attribute.dataTypeName"/>
+                        </div>
+                    </div>
+                    <div class="form-group" data-ng-class="{'has-error': attributeForm.name.$invalid
&& attributeForm.name.$dirty}" data-ng-show="attribute.$$show">
+                        <label for="attributeMultiplicity_{{$index}}" class="col-sm-2
control-label">Multiplicity</label>
+
+                        <div class="col-sm-10">
+                            <input type="text" class="form-control" name="name" id="attributeMultiplicity_{{$index}}"
placeholder="multiplicity"
+                                   data-ng-model="attribute.multiplicity"/>
+                        </div>
+                    </div>
+
+                    <div class="form-group" data-ng-class="{'has-error': attributeForm.name.$invalid
&& attributeForm.name.$dirty}" data-ng-show="attribute.$$show">
+                        <label for="attributeIscomposite_{{$index}}" class="col-sm-2 control-label">isComposite</label>
+
+                        <div class="col-sm-10">
+                            <span class ="btnToggle">
+                            <a class="btn-sm " ng-class="{true: 'btn-primary'}[attribute.isComposite]"
ng-click="attribute.isComposite = true">true </a>
+                            <a class="btn-sm " ng-class="{false: 'btn-danger'}[attribute.isComposite]"
ng-click="attribute.isComposite = false"> false </a>
+
+                                </span>
+                        </div>
+                    </div>
+                    <div class="form-group" data-ng-class="{'has-error': attributeForm.name.$invalid
&& attributeForm.name.$dirty}" data-ng-show="attribute.$$show">
+                        <label for="attributeIsunique_{{$index}}" class="col-sm-2 control-label">isUnique</label>
+
+                        <div class="col-sm-10">
+                            <span class ="btnToggle">
+                            <a class="btn-sm " ng-class="{true: 'btn-primary'}[attribute.isUnique]"
ng-click="attribute.isUnique = true">true </a>
+                            <a class="btn-sm " ng-class="{false: 'btn-danger'}[attribute.isUnique]"
ng-click="attribute.isUnique = false"> false </a>
+
+                                </span>
+                        </div>
+                    </div>
+                    <div class="form-group" data-ng-class="{'has-error': attributeForm.name.$invalid
&& attributeForm.name.$dirty}" data-ng-show="attribute.$$show">
+                        <label for="attributeIndexable_{{$index}}" class="col-sm-2 control-label">isIndexable</label>
+
+                        <div class="col-sm-10">
+                            <span class ="btnToggle">
+                            <a class="btn-sm " ng-class="{true: 'btn-primary'}[attribute.isIndexable]"
ng-click="attribute.isIndexable = true">true </a>
+                            <a class="btn-sm " ng-class="{false: 'btn-danger'}[attribute.isIndexable]"
ng-click="attribute.isIndexable = false"> false </a>
+
+                                </span>
+                        </div>
+                    </div>
+
+
+                    <div class="form-group" data-ng-class="{'has-error': attributeForm.name.$invalid
&& attributeForm.name.$dirty}" data-ng-show="attribute.$$show"
+                         data-ng-repeat-end>
+                        <label for="attributeReverseName_{{$index}}" class="col-sm-2 control-label">reverseAttributeName</label>
+
+                        <div class="col-sm-10">
+                            <input type="text" class="form-control" name="reverseName"
id="attributeReverseName_{{$index}}" placeholder="reverseAttributeName"
+                                   data-ng-model="attribute.reverseAttributeName"/>
+                        </div>
+                    </div>
+                </ng-form>
+                <div class="form-group text-right">
+                    <div class="col-sm-offset-2 col-sm-10">
+
+                        <button data-ng-click="addAttribute()" class="btn btn-default">Add
Attribute</button>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <div class="col-sm-offset-2 col-sm-10">
+                        <button type="submit" data-ng-click="save(tagForm)" data-ng-disabled="tagForm.$invalid"
class="btn btn-default">Save</button>
+                    </div>
+                </div>
+            </fieldset>
+        </form>
+        <div class="row" data-ng-show="savedTag">
+            <pre>{{savedTag | json}}</pre>
+        </div>
+    </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/538fa496/release-log.txt
----------------------------------------------------------------------
diff --git a/release-log.txt b/release-log.txt
index 3f0c205..a08332d 100644
--- a/release-log.txt
+++ b/release-log.txt
@@ -9,6 +9,7 @@ ATLAS-54 Rename configs in hive hook (shwethags)
 ATLAS-3 Mixed Index creation fails with Date types (sumasai via shwethags)
 
 ALL CHANGES:
+ATLAS-163 New Trait UI (Tag) ( darshankumar89 via sumasai)
 ATLAS-199 webapp build fails (grunt + tests) ( darshankumar89 via sumasai)
 ATLAS-204 Lineage I/O Lineage Enhancement ( Anilsg via sumasai )
 ATLAS-138 Combine Input/Output graph ( Anilsg via sumasai )


Mime
View raw message