Browse Source

Merge branch 'candidate-5.2.0'

Signed-off-by: Richard Chapman <rchapman@hpccsystems.com>
Richard Chapman 10 years ago
parent
commit
c6b5bb9253
54 changed files with 2030 additions and 93 deletions
  1. 4 0
      common/thorhelper/thorsoapcall.cpp
  2. 7 0
      ecl/hql/hqlexpr.cpp
  3. 1 0
      ecl/hql/hqlexpr.hpp
  4. 70 0
      ecl/hql/hqlmeta.cpp
  5. 2 0
      ecl/hql/hqlmeta.hpp
  6. 7 0
      ecl/hql/hqlutil.cpp
  7. 13 1
      ecl/hqlcpp/hqlcpp.cpp
  8. 2 0
      ecl/hqlcpp/hqlcpp.ipp
  9. 11 0
      ecl/hqlcpp/hqlhtcpp.cpp
  10. 15 0
      ecl/hqlcpp/hqlttcpp.cpp
  11. 6 5
      esp/scm/ws_topology.ecm
  12. 1 1
      esp/services/ws_topology/ws_topologyService.cpp
  13. 47 10
      esp/smc/SMCLib/TpWrapper.cpp
  14. 4 3
      esp/smc/SMCLib/TpWrapper.hpp
  15. 1 1
      esp/src/eclwatch/ActivityWidget.js
  16. 2 0
      esp/src/eclwatch/CurrentUserDetailsWidget.js
  17. 1 1
      esp/src/eclwatch/DFUWUDetailsWidget.js
  18. 3 3
      esp/src/eclwatch/ESPActivity.js
  19. 2 2
      esp/src/eclwatch/ESPDFUWorkunit.js
  20. 3 3
      esp/src/eclwatch/ESPLogicalFile.js
  21. 1 1
      esp/src/eclwatch/ESPPackageProcess.js
  22. 1 1
      esp/src/eclwatch/ESPQuery.js
  23. 1 1
      esp/src/eclwatch/ESPQueue.js
  24. 573 5
      esp/src/eclwatch/ESPTopology.js
  25. 155 0
      esp/src/eclwatch/ESPTree.js
  26. 55 12
      esp/src/eclwatch/ESPUtil.js
  27. 3 3
      esp/src/eclwatch/ESPWorkunit.js
  28. 8 0
      esp/src/eclwatch/FileSpray.js
  29. 13 0
      esp/src/eclwatch/GetDFUWorkunitsWidget.js
  30. 1 21
      esp/src/eclwatch/HPCCPlatformWidget.js
  31. 0 0
      esp/src/eclwatch/HelpersWidget.js
  32. 248 0
      esp/src/eclwatch/LogWidget.js
  33. 47 1
      esp/src/eclwatch/TargetSelectWidget.js
  34. 181 0
      esp/src/eclwatch/TopologyDetailsWidget.js
  35. 182 0
      esp/src/eclwatch/TopologyWidget.js
  36. 1 1
      esp/src/eclwatch/WUDetailsWidget.js
  37. 12 0
      esp/src/eclwatch/WUQueryWidget.js
  38. 25 0
      esp/src/eclwatch/WsSMC.js
  39. 104 2
      esp/src/eclwatch/WsTopology.js
  40. 1 1
      esp/src/eclwatch/_Widget.js
  41. BIN
      esp/src/eclwatch/img/cluster.png
  42. BIN
      esp/src/eclwatch/img/machine.png
  43. 13 0
      esp/src/eclwatch/nls/hpcc.js
  44. 3 3
      esp/src/eclwatch/templates/CurrentUserDetailsWidget.html
  45. 2 0
      esp/src/eclwatch/templates/HPCCPlatformOpsWidget.html
  46. 35 0
      esp/src/eclwatch/templates/LogWidget.html
  47. 20 0
      esp/src/eclwatch/templates/TopologyDetailsWidget.html
  48. 2 2
      esp/src/eclwatch/templates/WUDetailsWidget.html
  49. 12 9
      system/jlib/jcomp.cpp
  50. 2 0
      testing/regress/ecl/failrestart.ecl
  51. 73 0
      testing/regress/ecl/key/sort3.xml
  52. 2 0
      testing/regress/ecl/layouttrans_disabled.ecl
  53. 50 0
      testing/regress/ecl/sort3.ecl
  54. 2 0
      testing/regress/ecl/superfile6.ecl

+ 4 - 0
common/thorhelper/thorsoapcall.cpp

@@ -210,9 +210,13 @@ public:
                 path.append("/");
             }
         }
+        // While the code below would make some sense, there is code that relies on being able to use invalid IP addresses and catching the
+        // errors that result via ONFAIL.
+#if 0
         IpAddress ipaddr(host);
         if ( ipaddr.isNull())
             throw MakeStringException(-1, "Invalid IP address %s", host.str());
+#endif
     }
 };
 

+ 7 - 0
ecl/hql/hqlexpr.cpp

@@ -14777,6 +14777,13 @@ bool preservesValue(ITypeInfo * after, IHqlExpression * expr)
     return (recastValue->compare(value) == 0);
 }
 
+bool castPreservesValue(IHqlExpression * expr)
+{
+    dbgassertex(isCast(expr));
+    return preservesValue(expr->queryType(), expr->queryChild(0));
+}
+
+
 static const unsigned UNLIMITED_REPEAT = (unsigned)-1;
 
 unsigned getRepeatMin(IHqlExpression * expr)

+ 1 - 0
ecl/hql/hqlexpr.hpp

@@ -1538,6 +1538,7 @@ extern HQL_API unsigned getRepeatMin(IHqlExpression * expr);
 extern HQL_API bool isStandardRepeat(IHqlExpression * expr);
 extern HQL_API bool transformContainsCounter(IHqlExpression * transform, IHqlExpression * counter);
 extern HQL_API bool preservesValue(ITypeInfo * after, IHqlExpression * expr);
+extern HQL_API bool castPreservesValue(IHqlExpression * expr);
 extern HQL_API IHqlExpression * getActiveTableSelector();
 extern HQL_API IHqlExpression * queryActiveTableSelector();
 extern HQL_API IHqlExpression * getSelf(IHqlExpression * ds);

+ 70 - 0
ecl/hql/hqlmeta.cpp

@@ -3391,3 +3391,73 @@ extern HQL_API bool hasKnownSortGroupDistribution(IHqlExpression * expr, bool is
 {
     return queryMetaProperty(expr)->meta.hasKnownSortGroupDistribution(isLocal);
 }
+
+//---------------------------------------------------------------------------------------------------------------------
+
+//Mark all selectors that are fully included in the sort criteria. This may not catch all cases
+//but it is preferable to have false negatives than false positives.
+void markValidSelectors(IHqlExpression * expr, IHqlExpression * dsSelector)
+{
+    switch (expr->getOperator())
+    {
+    case no_sortlist:
+        break;
+    case no_cast:
+    case no_implicitcast:
+        if (!castPreservesValue(expr))
+            return;
+        break;
+    case no_typetransfer:
+        //Special case the transfer to a variable length data type that is done for a dataset in an index build
+        //(it will always preserve the value of any argument)
+        {
+            ITypeInfo * type = expr->queryType();
+            if ((type->getTypeCode() != type_data) || !isUnknownSize(type))
+                return;
+            break;
+        }
+    case no_select:
+        {
+            bool isNew = false;
+            IHqlExpression * root = querySelectorDataset(expr, isNew);
+            if (!isNew && (root == dsSelector))
+                expr->setTransformExtra(expr);
+            return;
+        }
+    default:
+        return;
+    }
+
+    ForEachChild(i, expr)
+        markValidSelectors(expr->queryChild(i), dsSelector);
+}
+
+extern HQL_API bool allFieldsAreSorted(IHqlExpression * record, IHqlExpression * sortOrder, IHqlExpression * dsSelector, bool strict)
+{
+    TransformMutexBlock block;
+
+    //First walk the sort order expression, tagging valid sorted selectors
+    markValidSelectors(sortOrder, dsSelector);
+
+    //Now expand all the selectors from the record, and check that they have been tagged
+    RecordSelectIterator iter(record, dsSelector);
+    ForEach(iter)
+    {
+        IHqlExpression * select = iter.query();
+        if (!select->queryTransformExtra())
+            return false;
+
+        if (strict && isUnknownSize(select->queryType()))
+        {
+            //Comparisons on strings (and unicode) ignore trailing spaces, so strictly speaking sorting by a variable
+            //length string field doesn't compare all the information from a field.
+            ITypeInfo * type = select->queryType();
+            if (type->getTypeCode() != type_data)
+            {
+                if (isStringType(type) || isUnicodeType(type))
+                    return false;
+            }
+        }
+    }
+    return true;
+}

+ 2 - 0
ecl/hql/hqlmeta.hpp

@@ -129,6 +129,8 @@ extern HQL_API CHqlMetaProperty * querySimpleDatasetMeta(IHqlExpression * expr);
 extern HQL_API bool hasSameSortGroupDistribution(IHqlExpression * expr, IHqlExpression * other);
 extern HQL_API bool hasKnownSortGroupDistribution(IHqlExpression * expr, bool isLocal);
 
+extern HQL_API bool allFieldsAreSorted(IHqlExpression * record, IHqlExpression * sortOrder, IHqlExpression * selector, bool strict);
+
 inline IHqlExpression * queryRemoveOmitted(IHqlExpression * expr)
 {
     if (expr &&  expr->isAttribute() && (expr->queryName() == _omitted_Atom))

+ 7 - 0
ecl/hql/hqlutil.cpp

@@ -5539,6 +5539,13 @@ IHqlExpression * extractCppBodyAttrs(unsigned lenBuffer, const char * buffer)
                         attrs.setown(createComma(attrs.getClear(), createAttribute(onceAtom)));
                     else if (matchOption(start, lenBuffer, buffer, 6, "action"))
                         attrs.setown(createComma(attrs.getClear(), createAttribute(actionAtom)));
+                    else if (matchOption(start, lenBuffer, buffer, 6, "source"))
+                    {
+                        stripQuotes(start, end, buffer);
+                        Owned<IValue> restOfLine = createUtf8Value(end-start, buffer+start, makeUtf8Type(UNKNOWN_LENGTH, NULL));
+                        OwnedHqlExpr arg = createConstant(restOfLine.getClear());
+                        attrs.setown(createComma(attrs.getClear(), createAttribute(sourceAtom, arg.getClear())));
+                    }
                     else if (matchOption(start, lenBuffer, buffer, 7, "library"))
                     {
                         stripQuotes(start, end, buffer);

+ 13 - 1
ecl/hqlcpp/hqlcpp.cpp

@@ -1210,10 +1210,11 @@ bool HqlCppInstance::useFunction(IHqlExpression * func)
     helpers.append(*LINK(func));
 
     IHqlExpression * funcDef = func->queryChild(0);
-    StringBuffer libname, init, include;
+    StringBuffer libname, init, include, source;
     getAttribute(funcDef, libraryAtom, libname);
     getAttribute(funcDef, initfunctionAtom, init);
     getAttribute(funcDef, includeAtom, include);
+    getAttribute(funcDef, sourceAtom, source);
     if (init.length())
     {
         BuildCtx ctx(*this, initAtom);
@@ -1240,6 +1241,8 @@ bool HqlCppInstance::useFunction(IHqlExpression * func)
     }
     if (include.length())
         useInclude(include.str());
+    if (source.length())
+        useSource(source);
     return true;
 }
 
@@ -1755,6 +1758,8 @@ void HqlCppTranslator::cacheOptions()
         DebugOption(options.newBalancedSpotter,"newBalancedSpotter",true),
         DebugOption(options.keyedJoinPreservesOrder,"keyedJoinPreservesOrder",true),
         DebugOption(options.expandSelectCreateRow,"expandSelectCreateRow",false),
+        DebugOption(options.optimizeSortAllFields,"optimizeSortAllFields",true),
+        DebugOption(options.optimizeSortAllFieldsStrict,"optimizeSortAllFieldsStrict",false),
     };
 
     //get options values from workunit
@@ -7454,6 +7459,13 @@ void HqlCppTranslator::processCppBodyDirectives(IHqlExpression * expr)
                 if (libraryName.length())
                     useLibrary(libraryName.str());
             }
+            else if (name == sourceAtom)
+            {
+                StringBuffer sourceName;
+                getStringValue(sourceName, cur->queryChild(0));
+                if (sourceName.length())
+                    code->useSource(sourceName.str());
+            }
         }
     }
 }

+ 2 - 0
ecl/hqlcpp/hqlcpp.ipp

@@ -745,6 +745,8 @@ struct HqlCppOptions
     bool                newBalancedSpotter;
     bool                keyedJoinPreservesOrder;
     bool                expandSelectCreateRow;
+    bool                optimizeSortAllFields;
+    bool                optimizeSortAllFieldsStrict;
 };
 
 //Any information gathered while processing the query should be moved into here, rather than cluttering up the translator class

+ 11 - 0
ecl/hqlcpp/hqlhtcpp.cpp

@@ -16387,6 +16387,17 @@ ABoundActivity * HqlCppTranslator::doBuildActivitySort(BuildCtx & ctx, IHqlExpre
         flags.append("|TAFunstable");
         method = unstable->queryChild(0);
     }
+    else
+    {
+        //If a dataset is sorted by all fields then it is impossible to determine if the original order
+        //was preserved - so mark the sort as potentially unstable (to reduce memory usage at runtime)
+        if (options.optimizeSortAllFields &&
+            allFieldsAreSorted(expr->queryRecord(), sortlist, dataset->queryNormalizedSelector(), options.optimizeSortAllFieldsStrict))
+        {
+            flags.append("|TAFunstable");
+        }
+    }
+
     if (spill)
         flags.append("|TAFspill");
     if (!method || method->isConstant())

+ 15 - 0
ecl/hqlcpp/hqlttcpp.cpp

@@ -1778,6 +1778,21 @@ static IHqlExpression * simplifySortlistComplexity(IHqlExpression * sortlist)
                     appendComponent(cpts, invert, &concats.item(idxc));
             }
         }
+        else if (cur->getOperator() == no_trim)
+        {
+            //Strings are always compared as if they are trimmed (or padded with arbitrary spaces)
+            //=> sort by TRIM(string) can just sort by the string instead.  (Don't match LEFT/RIGHT versions.)
+            if (!cur->queryChild(1))
+            {
+                IHqlExpression * arg = cur->queryChild(0);
+                ITypeInfo * argType = arg->queryType();
+                if (cur->queryType()->getTypeCode() == argType->getTypeCode())
+                {
+                    expand = true;
+                    appendComponent(cpts, invert, arg);
+                }
+            }
+        }
         else
         {
 #if 0

+ 6 - 5
esp/scm/ws_topology.ecm

@@ -63,10 +63,11 @@ ESPStruct TpLogicalCluster
 
 //  ===========================================================================
 
-ESPStruct TpGroup
+ESPStruct [nil_remove] TpGroup
 {
     string Name;
-    string Prefix;
+    [min_ver("1.21")] string Kind;
+    [min_ver("1.21")] bool ReplicateOutputs;
 };
 
 //  ===========================================================================
@@ -353,9 +354,9 @@ ESPresponse [exceptions_inline] TpLogicalClusterQueryResponse
 };
 
 
-ESPrequest TpGroupQueryRequest
+ESPrequest [nil_remove] TpGroupQueryRequest
 {
-    
+    [min_ver("1.21")] string Kind;
 };
 
 ESPresponse [exceptions_inline] TpGroupQueryResponse
@@ -582,7 +583,7 @@ ESPresponse [exceptions_inline,encode(0)] TpGetServicePluginsResponse
     ESParray<ESPstruct TpEspServicePlugin, Plugin> Plugins;
 };
 
-ESPservice [noforms, version("1.20"), default_client_version("1.20"), exceptions_inline("./smc_xslt/exceptions.xslt")] WsTopology
+ESPservice [noforms, version("1.21"), default_client_version("1.20"), exceptions_inline("./smc_xslt/exceptions.xslt")] WsTopology
 {
     ESPuses ESPStruct TpBinding;
     ESPuses ESPstruct TpCluster;

+ 1 - 1
esp/services/ws_topology/ws_topologyService.cpp

@@ -1212,7 +1212,7 @@ bool CWsTopologyEx::onTpGroupQuery(IEspContext &context, IEspTpGroupQueryRequest
             throw MakeStringException(ECLWATCH_TOPOLOGY_ACCESS_DENIED, "Failed to do Group Query. Permission denied.");
 
         IArrayOf<IEspTpGroup> Groups;
-        m_TpWrapper.getGroupList(Groups);
+        m_TpWrapper.getGroupList(context.getClientVersion(), req.getKind(), Groups);
         resp.setTpGroups(Groups);
     }
     catch(IException* e)

+ 47 - 10
esp/smc/SMCLib/TpWrapper.cpp

@@ -1320,32 +1320,69 @@ void CTpWrapper::getHthorClusterList(IArrayOf<IEspTpCluster>& clusterList)
 }
 
 
-void CTpWrapper::getGroupList(IArrayOf<IEspTpGroup> &GroupList)
+void CTpWrapper::getGroupList(double espVersion, const char* kindReq, IArrayOf<IEspTpGroup> &GroupList)
 {
     try
     {
         Owned<IRemoteConnection> conn = querySDS().connect("/Groups", myProcessSession(), RTM_LOCK_READ, SDS_LOCK_TIMEOUT);
         Owned<IPropertyTreeIterator> groups= conn->queryRoot()->getElements("Group");
-        if (groups->first()) {
-            do {
+        if (groups->first())
+        {
+            do
+            {
                 IPropertyTree &group = groups->query();
+                const char* kind = group.queryProp("@kind");
+                if (kindReq && *kindReq && !strieq(kindReq, kind))
+                    continue;
+
                 IEspTpGroup* pGroup = createTpGroup("","");
-                pGroup->setName(group.queryProp("@name"));
+                const char* name = group.queryProp("@name");
+                pGroup->setName(name);
+                if (espVersion >= 1.21)
+                {
+                    pGroup->setKind(kind);
+                    pGroup->setReplicateOutputs(checkGroupReplicateOutputs(name, kind));
+                }
                 GroupList.append(*pGroup);
-                
             } while (groups->next());
         }
     }
-    catch(IException* e){   
-      StringBuffer msg;
-      e->errorMessage(msg);
+    catch(IException* e)
+    {
+        StringBuffer msg;
+        e->errorMessage(msg);
         WARNLOG("%s", msg.str());
         e->Release();
     }
-    catch(...){
+    catch(...)
+    {
         WARNLOG("Unknown Exception caught within CTpWrapper::getGroupList");
     }
-    StringBuffer cluster;
+}
+
+bool CTpWrapper::checkGroupReplicateOutputs(const char* groupName, const char* kind)
+{
+    if (strieq(kind, "Roxie") || strieq(kind, "hthor"))
+        return false;
+
+    Owned<IEnvironmentFactory> factory = getEnvironmentFactory();
+    Owned<IConstEnvironment> environment = factory->openEnvironment();
+    if (!environment)
+        throw MakeStringException(ECLWATCH_CANNOT_GET_ENV_INFO, "Failed to get environment information.");
+    Owned<IPropertyTree> root = &environment->getPTree();
+    if (!root)
+        throw MakeStringException(ECLWATCH_CANNOT_GET_ENV_INFO, "Failed to get environment information.");
+
+    Owned<IPropertyTreeIterator> it= root->getElements("Software/ThorCluster");
+    ForEach(*it)
+    {
+        StringBuffer thorClusterGroupName;
+        IPropertyTree& cluster = it->query();
+        getClusterGroupName(cluster, thorClusterGroupName);
+        if (thorClusterGroupName.length() && strieq(thorClusterGroupName.str(), groupName))
+            return cluster.getPropBool("@replicateOutputs", false);
+    }
+    return false;
 }
 
 void CTpWrapper::resolveGroupInfo(const char* groupName,StringBuffer& Cluster, StringBuffer& ClusterPrefix)

+ 4 - 3
esp/smc/SMCLib/TpWrapper.hpp

@@ -136,8 +136,9 @@ private:
     void getAttPath(const char* Path,StringBuffer& returnStr);
     bool ContainsProcessDefinition(IPropertyTree& node,const char* clusterName);
     const char* getNodeNameTag(const char* MachineType);
-   void fetchInstances(const char* ServiceType, IPropertyTree& service, IArrayOf<IEspTpMachine>& tpMachines);
-    
+    void fetchInstances(const char* ServiceType, IPropertyTree& service, IArrayOf<IEspTpMachine>& tpMachines);
+    bool checkGroupReplicateOutputs(const char* groupName, const char* kind);
+
 public:
     IMPLEMENT_IINTERFACE;
     CTpWrapper() {};
@@ -146,7 +147,7 @@ public:
     bool getClusterLCR(const char* clusterType, const char* clusterName);
     void getClusterProcessList(const char* ClusterType, IArrayOf<IEspTpCluster>& clusters, bool ignoreduplicatqueues=false, bool ignoreduplicategroups=false);
     void getHthorClusterList(IArrayOf<IEspTpCluster>& clusterList);
-    void getGroupList(IArrayOf<IEspTpGroup> &Groups);
+    void getGroupList(double espVersion, const char* kindReq, IArrayOf<IEspTpGroup> &Groups);
     void getCluster(const char* ClusterType,IPropertyTree& returnRoot);
     void getClusterMachineList(double clientVersion, const char* ClusterType,const char* ClusterPath, const char* ClusterDirectory, 
                                         IArrayOf<IEspTpMachine> &MachineList, bool& hasThorSpareProcess, const char* ClusterName = NULL);

+ 1 - 1
esp/src/eclwatch/ActivityWidget.js

@@ -187,7 +187,7 @@ define([
                 return;
 
             var context = this;
-            this.activity.watch("changedCount", function (item, oldValue, newValue) {
+            this.activity.watch("__hpcc_changedCount", function (item, oldValue, newValue) {
                 context.grid.set("query", {});
                 context._refreshActionState();
             });

+ 2 - 0
esp/src/eclwatch/CurrentUserDetailsWidget.js

@@ -66,6 +66,7 @@ define([
 
         //  Hitched actions  ---
         _onSave: function (event) {
+            var dialog = dijit.byId("stubUserDialog");
             if (this.userForm.validate()) {
                 var formInfo = domForm.toObject(this.id + "UserForm");
                 WsAccount.UpdateUser({
@@ -77,6 +78,7 @@ define([
                         newpass2: formInfo.newPassword
                     }
                 });
+                dialog.hide();
             }
         },
 

+ 1 - 1
esp/src/eclwatch/DFUWUDetailsWidget.js

@@ -264,7 +264,7 @@ define([
                 case "hasCompleted":
                     this.refreshActionState();
                     break;
-                case "changedCount":
+                case "__hpcc_changedCount":
                     if (this.wu.SourceLogicalName) {
                         this.ensurePane("SourceLogicalName", this.i18n.Source, {
                             NodeGroup: this.wu.SourceGroupName,

+ 3 - 3
esp/src/eclwatch/ESPActivity.js

@@ -98,12 +98,12 @@ define([
         },
 
         monitor: function (callback) {
-            if (callback && this.changedCount) {
+            if (callback && this.__hpcc_changedCount) {
                 callback(this);
             }
             if (!this.hasCompleted) {
                 var context = this;
-                this.watch("changedCount", function (name, oldValue, newValue) {
+                this.watch("__hpcc_changedCount", function (name, oldValue, newValue) {
                     if (oldValue !== newValue && newValue) {
                         if (callback) {
                             callback(context);
@@ -149,7 +149,7 @@ define([
                     targetClusters.push(queue);
                     targetClusterMap[queue.__hpcc_id] = queue;
                     if (!context._watched[queue.__hpcc_id]) {
-                        context._watched[queue.__hpcc_id] = queue.watch("changedCount", function (name, oldValue, newValue) {
+                        context._watched[queue.__hpcc_id] = queue.watch("__hpcc_changedCount", function (name, oldValue, newValue) {
                             if (oldValue !== newValue) {
                                 if (context.observableStore.get(queue.__hpcc_id)) {
                                     context.observableStore.notify(queue, queue.__hpcc_id);

+ 2 - 2
esp/src/eclwatch/ESPDFUWorkunit.js

@@ -69,7 +69,7 @@ define([
             storeItem.updateData(item);
             if (!this._watched[id]) {
                 var context = this;
-                this._watched[id] = storeItem.watch("changedCount", function (name, oldValue, newValue) {
+                this._watched[id] = storeItem.watch("__hpcc_changedCount", function (name, oldValue, newValue) {
                     if (oldValue !== newValue) {
                         context.notify(storeItem, id);
                     }
@@ -163,7 +163,7 @@ define([
             }
             if (!this.hasCompleted) {
                 var context = this;
-                this.watch("changedCount", function (name, oldValue, newValue) {
+                this.watch("__hpcc_changedCount", function (name, oldValue, newValue) {
                     if (oldValue !== newValue && newValue) {
                         if (callback) {
                             callback(context);

+ 3 - 3
esp/src/eclwatch/ESPLogicalFile.js

@@ -76,7 +76,7 @@ define([
             storeItem.updateData(item);
             if (!this._watched[id]) {
                 var context = this;
-                this._watched[id] = storeItem.watch("changedCount", function (name, oldValue, newValue) {
+                this._watched[id] = storeItem.watch("__hpcc_changedCount", function (name, oldValue, newValue) {
                     if (oldValue !== newValue) {
                         context.notify(storeItem, id);
                     }
@@ -106,7 +106,7 @@ define([
 
         _fetchFiles: function (scope) {
             var context = this;
-            results = WsDfu.DFUFileView({
+            var results = WsDfu.DFUFileView({
                 request: {
                     Scope: scope
                 }
@@ -143,7 +143,7 @@ define([
                         } else {
                             storeItem = create(item.__hpcc_id);
                             if (!context._watched[item.__hpcc_id]) {
-                                context._watched[item.__hpcc_id] = storeItem.watch("changedCount", function (name, oldValue, newValue) {
+                                context._watched[item.__hpcc_id] = storeItem.watch("__hpcc_changedCount", function (name, oldValue, newValue) {
                                     if (oldValue !== newValue) {
                                         context.notify(storeItem, storeItem.__hpcc_id);
                                     }

+ 1 - 1
esp/src/eclwatch/ESPPackageProcess.js

@@ -121,7 +121,7 @@ define([
                         var packageMap = context.get(item.Id);
                         packageMap.updateData(item);
                         packageMaps.push(packageMap);
-                        context._watched[packageMap.Id] = packageMap.watch("changedCount", function (name, oldValue, newValue) {
+                        context._watched[packageMap.Id] = packageMap.watch("__hpcc_changedCount", function (name, oldValue, newValue) {
                             if (oldValue !== newValue) {
                                 context.notify(packageMap, packageMap.Id);
                             }

+ 1 - 1
esp/src/eclwatch/ESPQuery.js

@@ -62,7 +62,7 @@ define([
             storeItem.updateData(item);
             if (!this._watched[id]) {
                 var context = this;
-                this._watched[id] = storeItem.watch("changedCount", function (name, oldValue, newValue) {
+                this._watched[id] = storeItem.watch("__hpcc_changedCount", function (name, oldValue, newValue) {
                     if (oldValue !== newValue) {
                         context.notify(storeItem, id);
                     }

+ 1 - 1
esp/src/eclwatch/ESPQueue.js

@@ -151,7 +151,7 @@ define([
             }
             if (!this._watched[wu.__hpcc_id]) {
                 var context = this;
-                this._watched[wu.__hpcc_id] = wu.watch("changedCount", function (name, oldValue, newValue) {
+                this._watched[wu.__hpcc_id] = wu.watch("__hpcc_changedCount", function (name, oldValue, newValue) {
                     if (oldValue !== newValue) {
                         //  If child changes force the parent to refresh...
                         context.updateData({

+ 573 - 5
esp/src/eclwatch/ESPTopology.js

@@ -16,11 +16,19 @@
 define([
     "dojo/_base/declare",
     "dojo/_base/lang",
+    "dojo/_base/array",
+    "dojo/_base/Deferred",
+    "dojo/promise/all",
+    "dojo/store/Memory",
+    "dojo/store/Observable",
+    "dojo/store/util/QueryResults",
 
     "hpcc/WsTopology",
-    "hpcc/ESPUtil"
-], function (declare, lang,
-    WsTopology, ESPUtil) {
+    "hpcc/WsSMC",
+    "hpcc/ESPUtil",
+    "hpcc/ESPTree"
+], function (declare, lang, arrayUtil, Deferred, all, Memory, Observable, QueryResults,
+    WsTopology, WsSMC, ESPUtil, ESPTree) {
 
     var ThorCache = {
     };
@@ -51,6 +59,566 @@ define([
         }
     });
 
+    var createTreeItem = function (Type, id, espParent, data) {
+        if (!(espParent instanceof TopologyItem)) {
+            if (!espParent && id !== "root") {
+                var d = 0;
+            }
+        }
+        var retVal = new Type({ __hpcc_id: id, __hpcc_parent: espParent });
+        if (data) {
+            retVal.updateData(data);
+        }
+        return retVal;
+    };
+
+
+    var TopologyItem = declare([ESPTree.Item], {
+        constructor: function (args) {
+            this.__hpcc_children = [];
+        },
+
+        appendChild: function (child) {
+            this.__hpcc_children.push(child);
+        },
+
+        appendChildren: function (children) {
+            arrayUtil.forEach(children, function (child) {
+                this.appendChild(child);
+            }, this);
+        },
+
+        getLabel: function () {
+            return this.__hpcc_displayName;
+        },
+
+        //  Helpers  ---
+        getCompType: function () {
+            if (this.__hpcc_parent && this.__hpcc_parent.Type) {
+                return this.__hpcc_parent.Type;
+            } else {
+                return this.Type;
+            }
+        },
+
+        getCompName: function () {
+            if (this.__hpcc_parent && this.__hpcc_parent.Name) {
+                return this.__hpcc_parent.Name;
+            } else {
+                return this.Name;
+            }
+        },
+
+        getLogDirectory: function () {
+            if (this.LogDirectory) {
+                return this.LogDirectory;
+            } else if (this.__hpcc_parent) {
+                if (this.__hpcc_parent.LogDir) {
+                    return this.__hpcc_parent.LogDir;
+                } else if (this.__hpcc_parent.LogDirectory) {
+                    return this.__hpcc_parent.LogDirectory;
+                }
+            }
+            return "";
+        }
+    });
+
+    var TpMachine = declare([TopologyItem], {
+        __hpcc_type: "TpMachine",
+        constructor: function (args) {
+        },
+        getIcon: function () {
+            return "machine.png";
+        },
+        updateData: function () {
+            this.inherited(arguments);
+            this.__hpcc_displayName = "[" + this.Netaddress + "] " + this.Name;
+        }
+    });
+
+    var TpCommon = declare([TopologyItem], {
+        _TpMachinesSetter: function (TpMachines) {
+            if (lang.exists("TpMachine", TpMachines)) {
+                arrayUtil.forEach(TpMachines.TpMachine, function (item, idx) {
+                    var newMachine = createTreeItem(TpMachine, item.Type + "_" + item.Netaddress + "_" + item.ProcessNumber + "_" + item.Directory, this, item)
+                    this.appendChild(newMachine);
+                }, this);
+            }
+        },
+        updateData: function () {
+            this.inherited(arguments);
+            this.__hpcc_displayName = "[" + (this.Type ? this.Type : this.__hpcc_type) + "] " + this.Name;
+        }
+    });
+
+    var TpService = declare([TpCommon], {
+        __hpcc_type: "TpService",
+        constructor: function (args) {
+            var d = 0;
+        },
+        getLabel: function () {
+            return "[" + this.Type + "] " + this.Name;
+        }
+    });
+
+    var TpEclAgent = declare([TpService], {
+        __hpcc_type: "TpEclAgent",
+        constructor: function (args) {
+            var d = 0;
+        }
+    });
+
+    var TpEclServer = declare([TpService], {
+        __hpcc_type: "TpEclServer",
+        constructor: function (args) {
+            var d = 0;
+        }
+    });
+
+    var TpEclCCServer = declare([TpService], {
+        __hpcc_type: "TpEclCCServer",
+        constructor: function (args) {
+            var d = 0;
+        }
+    });
+
+    var TpEclScheduler = declare([TpService], {
+        __hpcc_type: "TpEclScheduler",
+        constructor: function (args) {
+            var d = 0;
+        }
+    });
+
+    var TpBinding = declare([TpCommon], {
+        __hpcc_type: "TpBinding",
+        constructor: function (args) {
+        },
+
+        getLabel: function () {
+            return this.Service;
+        }
+    });
+
+    var Cluster = declare([TpCommon], {
+        __hpcc_type: "Cluster",
+        constructor: function (args) {
+        },
+        getIcon: function () {
+            return "cluster.png";
+        },
+        getLabel: function () {
+            return this.Name;
+        }
+    });
+
+    var Service = declare([TpCommon], {
+        __hpcc_type: "Service",
+        constructor: function (args) {
+        },
+        _TpBindingsSetter: function (TpBindings) {
+            if (lang.exists("TpBinding", TpBindings)) {
+                arrayUtil.forEach(TpBindings.TpBinding, function (item, idx) {
+                    this.appendChild(createTreeItem(TpBinding, item.Service + "::" + item.Port, this, item));
+                }, this);
+            }
+        }
+    });
+
+    var ServiceType = declare([TpCommon], {
+        __hpcc_type: "ServiceType",
+        constructor: function (args) {
+            var d= 0;
+        },
+        getIcon: function () {
+            return "folder.png";
+        },
+        getLabel: function () {
+            switch (this.__hpcc_id) {
+                case "ServiceType::TpDali":
+                    return "Dali";
+                case "ServiceType::TpDfuServer":
+                    return "DFU Server";
+                case "ServiceType::TpDropZone":
+                    return "Drop Zone";
+                case "ServiceType::TpEclAgent":
+                    return "ECL Agent";
+                case "ServiceType::TpEclCCServer":
+                    return "ECLCC Server";
+                case "ServiceType::TpEclServer":
+                    return "ECL Server";
+                case "ServiceType::TpEclScheduler":
+                    return "ECL Scheduler";
+                case "ServiceType::TpEspServer":
+                    return "ESP Server";
+                case "ServiceType::TpFTSlave":
+                    return "FT Slave";
+                case "ServiceType::TpSashaServer":
+                    return "Sasha";
+            }
+            return "Unknown";
+        },
+
+        addServices: function (items) {
+            arrayUtil.forEach(items, function (item) {
+                this.appendChild(createTreeItem(Service, item.Name, this, item));
+            }, this);
+            return this;
+        }
+    });
+
+    var Services = declare([TpCommon], {
+        __hpcc_type: "Services",
+        constructor: function (args) {
+            args.__hpcc_displayName = "Services";
+        },
+
+        getIcon: function () {
+            return "folder.png";
+        },
+
+        getLabel: function () {
+            return "Services";
+        },
+
+        appendServiceType: function (property, data) {
+            if (lang.exists(property, data)) {
+                var newServiceType = createTreeItem(ServiceType, property, this);
+                newServiceType.addServices(data[property]);
+                this.appendChild(newServiceType);
+            } else {
+                throw "GJS";
+            }
+        },
+
+        _TpDalisSetter: function (TpDalis) {
+            this.appendServiceType("TpDali", TpDalis);
+        },
+        _TpDfuServersSetter: function (TpDfuServers) {
+            this.appendServiceType("TpDfuServer", TpDfuServers);
+        },
+        _TpDropZonesSetter: function (TpDropZones) {
+            this.appendServiceType("TpDropZone", TpDropZones);
+        },
+        _TpEclAgentsSetter: function (TpEclAgents) {
+            this.appendServiceType("TpEclAgent", TpEclAgents);
+        },
+        _TpEclServersSetter: function (TpEclServers) {
+            this.appendServiceType("TpEclServer", TpEclServers);
+        },
+        _TpEclCCServersSetter: function (TpEclCCServers) {
+            this.appendServiceType("TpEclServer", TpEclCCServers);
+        },
+        _TpEclSchedulersSetter: function (TpEclSchedulers) {
+            this.appendServiceType("TpEclScheduler", TpEclSchedulers);
+        },
+        _TpEspServersSetter: function (TpEspServers) {
+            this.appendServiceType("TpEspServer", TpEspServers);
+        },
+        _TpFTSlavesSetter: function (TpFTSlaves) {
+            this.appendServiceType("TpFTSlave", TpFTSlaves);
+        },
+        _TpSashaServersSetter: function (TpSashaServers) {
+            this.appendServiceType("TpSashaServer", TpSashaServers);
+        }
+    });
+
+    var TargetCluster = declare([TpCommon], {
+        __hpcc_type: "TargetCluster",
+        constructor: function (args) {
+        },
+        getIcon: function () {
+            return "server.png";
+        },
+        getLabel: function () {
+            return this.Name;
+        },
+        _TpEclAgentsSetter: function (TpEclAgents) {
+            var context = this;
+            if (lang.exists("TpEclAgent", TpEclAgents)) {
+                arrayUtil.forEach(TpEclAgents.TpEclAgent, function (item, idx) {
+                    this.appendChild(createTreeItem(TpEclAgent, item.Name, this, item));
+                }, this);
+            }
+        },
+
+        _TpEclCCServersSetter: function (TpEclCCServers) {
+            if (lang.exists("TpEclServer", TpEclCCServers)) {
+                arrayUtil.forEach(TpEclCCServers.TpEclServer, function (item, idx) {
+                    this.appendChild(createTreeItem(TpEclCCServer, item.Name, this, item));
+                }, this);
+            }
+        },
+
+        _TpEclServersSetter: function (TpEclServers) {
+            if (lang.exists("TpEclServer", TpEclServers)) {
+                arrayUtil.forEach(TpEclServers.TpEclServer, function (item, idx) {
+                    this.appendChild(createTreeItem(TpEclServer, item.Name, this, item));
+                }, this);
+            }
+        },
+
+        _TpEclSchedulersSetter: function (TpEclSchedulers) {
+            if (lang.exists("TpEclScheduler", TpEclSchedulers)) {
+                arrayUtil.forEach(TpEclSchedulers.TpEclScheduler, function (item, idx) {
+                    this.appendChild(createTreeItem(TpEclScheduler, item.Name, this, item));
+                }, this);
+            }
+        },
+
+        _TpClustersSetter: function (TpClusters) {
+            if (lang.exists("TpCluster", TpClusters)) {
+                arrayUtil.forEach(TpClusters.TpCluster, function (item, idx) {
+                    this.appendChild(createTreeItem(Cluster, item.Name, this, item));
+                }, this);
+            }
+        }
+    });
+
+    var TargetClusterType = declare([TpCommon], {
+        __hpcc_type: "TargetClusterType",
+        constructor: function (args) {
+            args.__hpcc_displayName = "TargetClusterType";
+        },
+
+        getIcon: function () {
+            return "folder.png";
+        },
+
+        getLabel: function () {
+            return this.Name;
+        }
+    });
+
+    var TopologyRoot = declare([TopologyItem], {
+        __hpcc_type: "TopologyRoot",
+        getIcon: function () {
+            return "workunit.png";
+        },
+        getLabel: function () {
+            return "Topology";
+        }
+    });
+
+    var TopologyTreeStore = declare([ESPTree.Store], {
+        constructor: function() {
+            this.viewMode("Debug");
+            this.cachedTreeItems = {};
+            this.cachedRelations = {};
+            this.cachedRelationsPC = {};
+        },
+        createTreeNode: function (parentNode, treeItem) {
+            var retVal = this.inherited(arguments);
+            retVal.hasConfig = function () {
+                return this.__hpcc_treeItem.Netaddress && this.__hpcc_treeItem.Directory;
+            };
+            retVal.getConfig = function () {
+                return WsTopology.TpGetComponentFile({
+                    request: {
+                        CompType: this.__hpcc_treeItem.getCompType(),
+                        CompName: this.__hpcc_treeItem.getCompName(),
+                        NetAddress: this.__hpcc_treeItem.Netaddress,
+                        Directory: this.__hpcc_treeItem.Directory,
+                        FileType: "cfg",
+                        OsType: this.__hpcc_treeItem.OS
+                    }
+                });
+            };
+            retVal.hasLogs = function () {
+                return this.__hpcc_treeItem.getLogDirectory && this.__hpcc_treeItem.getLogDirectory();
+            };
+            return retVal;
+        },
+        clear: function () {
+            this.inherited(arguments);
+            this.cachedTreeItems = {};
+            this.cachedRelations = {};
+            this.cachedRelationsPC = {};
+        },
+        viewMode: function (mode) {
+            this._viewMode = mode;
+        },
+        createTreeItemXXX: function (Type, id, data) {
+            var newItem = new Type({ __hpcc_store: this, __hpcc_id: id });
+            var retVal = this.cachedTreeItems[newItem.getUniqueID()];
+            if (!retVal) {
+                retVal = newItem;
+                this.cachedTreeItems[newItem.getUniqueID()] = retVal;
+                this.cachedRelationsPC[newItem.getUniqueID()] = [];
+            } else {
+                //  Sanity Checking  ---
+                for (key in data) {
+                    if (!(data[key] instanceof Object)) {
+                        if (retVal.get(key) != data[key] && key !== "HasThorSpareProcess") {
+                            var d = 0;//throw "Duplicate ID";
+                        }
+                    }
+                }
+            }
+            if (data) {
+                retVal.updateData(data);
+            }
+            return retVal;
+        },
+        query: function (query, options) {
+            var data = [];
+            if (this.rootItem) {
+                switch (this._viewMode) {
+                    case "Debug":
+                        data.push(this.createTreeNode(null, this.rootItem));
+                        break;
+                    case "Targets":
+                        arrayUtil.forEach(this.rootItem.__hpcc_children, function (item) {
+                            if (item.__hpcc_type === "TargetClusterType") {
+                                data.push(this.createTreeNode(null, item));
+                            }
+                        }, this);
+                        break;
+                    case "Services":
+                        arrayUtil.forEach(this.rootItem.__hpcc_children, function (item) {
+                            if (item.__hpcc_type === "Services") {
+                                arrayUtil.forEach(item.__hpcc_children, function (item2) {
+                                    if (item2.__hpcc_type === "ServiceType") {
+                                        data.push(this.createTreeNode(null, item2));
+                                    }
+                                }, this);
+                            }
+                        }, this);
+                        break;
+                    case "Machines":
+                        var instance = {};
+                        var machines = {};
+                        var context = this;
+                        function getMachines(treeItem, parentTreeItem) {
+                            if (treeItem instanceof TpMachine) {
+                                if (!machines[treeItem.Netaddress]) {
+                                    var machineNode = context.createTreeNode(null, treeItem);
+                                    machines[treeItem.Netaddress] = machineNode;
+                                    data.push(machineNode);
+                                }
+                                if (parentTreeItem) {
+                                    if (!instance[treeItem.getUniqueID()]) {
+                                        instance[treeItem.getUniqueID()] = true;
+                                        context.createTreeNode(machines[treeItem.Netaddress], parentTreeItem);
+                                    }
+                                }
+                            }
+                            arrayUtil.forEach(treeItem.__hpcc_children, function (child) {
+                                getMachines(child, treeItem);
+                            }, this);
+                        }
+                        getMachines(this.rootItem);
+                        data.sort(function (a, b) {
+                            aa = a.__hpcc_treeItem.Netaddress.split(".");
+                            bb = b.__hpcc_treeItem.Netaddress.split(".");
+                            var resulta = aa[0] * 0x1000000 + aa[1] * 0x10000 + aa[2] * 0x100 + aa[3] * 1;
+                            var resultb = bb[0] * 0x1000000 + bb[1] * 0x10000 + bb[2] * 0x100 + bb[3] * 1;
+                            return resulta - resultb;
+                        });
+                        break;
+                }
+            }
+            return QueryResults(this.queryEngine({}, {})(data));
+        },
+
+        mayHaveChildren: function (treeNode) {
+            return this.getChildren(treeNode, {}).length > 0;
+        },
+
+        getChildren: function (treeNode, options) {
+            var data = [];
+            if (treeNode.__hpcc_children.length) {
+                data = treeNode.__hpcc_children;
+            } else {
+                switch (this._viewMode) {
+                    case "Targets":
+                        data = arrayUtil.map(treeNode.__hpcc_treeItem.__hpcc_children, function (item) {
+                            return this.createTreeNode(treeNode, item);
+                        }, this);
+                        break;
+                    case "Services":
+                        if (!treeNode.__hpcc_parentNode) {
+                            arrayUtil.forEach(treeNode.__hpcc_treeItem.__hpcc_children, function (child) {
+                                var serviceNode = this.createTreeNode(treeNode, child);
+                                var machines = [];
+                                var bindings = [];
+                                arrayUtil.forEach(child.__hpcc_children, function (gchild) {
+                                    if (gchild instanceof TpMachine) {
+                                        machines.push(gchild);
+                                    } else if (gchild instanceof TpBinding) {
+                                        bindings.push(gchild);
+                                    }
+                                }, this);
+                                arrayUtil.forEach(bindings, function (binding) {
+                                    var bindingNode = this.createTreeNode(serviceNode, binding);
+                                    arrayUtil.forEach(machines, function (machine) {
+                                        this.createTreeNode(bindingNode, machine);
+                                    }, this);
+                                }, this);
+                                arrayUtil.forEach(machines, function (machine) {
+                                    var machineNode = this.createTreeNode(serviceNode, machine);
+                                    arrayUtil.forEach(bindings, function (binding) {
+                                        this.createTreeNode(machineNode, binding);
+                                    }, this);
+                                }, this);
+                                data.push(serviceNode);
+                            }, this);
+                        }
+                        break;
+                    case "Debug":
+                        data = arrayUtil.map(treeNode.__hpcc_treeItem.__hpcc_children, function (item) {
+                            return this.createTreeNode(treeNode, item);
+                        }, this);
+                        break;
+                    default:
+                        break;
+                }
+            }
+            return QueryResults(this.queryEngine({}, {})(data));
+        },
+
+        refresh: function (callback) {
+            this.clear();
+            this.rootItem = createTreeItem(TopologyRoot, "root");
+
+            var calls = [];
+            var context = this;
+            return all({
+                targetClusterQuery: WsTopology.TpTargetClusterQuery({
+                    request: {
+                        Type: "ROOT"
+                    }
+                }).then(function (response) {
+                    var clusterTypes = {};
+                    var retVal = [];
+                    if (lang.exists("TpTargetClusterQueryResponse.TpTargetClusters", response)) {
+                        arrayUtil.forEach(response.TpTargetClusterQueryResponse.TpTargetClusters.TpTargetCluster, function (item, idx) {
+                            if (!clusterTypes[item.Type]) {
+                                clusterTypes[item.Type] = createTreeItem(TargetClusterType, item.Type, context.rootItem, { Name: item.Type })
+                                retVal.push(clusterTypes[item.Type]);
+                            }
+                            clusterTypes[item.Type].appendChild(createTreeItem(TargetCluster, item.Name, context.rootItem, item));
+                        }, this);
+                    }
+                    return retVal;
+                }),
+                serviceQuery: WsTopology.TpServiceQuery({
+                    request: {
+                        Type: "ALLSERVICES"
+                    }
+                }).then(function (response) {
+                    var retVal = [];
+                    if (lang.exists("TpServiceQueryResponse.ServiceList", response)) {
+                        retVal.push(createTreeItem(Services, "Services", context.rootItem, response.TpServiceQueryResponse.ServiceList));
+                    }
+                    return retVal;
+                })
+            }).then(function (responses) {
+                context.rootItem.appendChildren(responses.targetClusterQuery);
+                context.rootItem.appendChildren(responses.serviceQuery);
+                callback();
+            });
+        }
+    });
 
     return {
         GetThor: function (thorName) {
@@ -60,7 +628,7 @@ define([
                 });
             }
             return ThorCache[thorName];
-        }
-
+        },
+        Store: TopologyTreeStore
     };
 });

+ 155 - 0
esp/src/eclwatch/ESPTree.js

@@ -0,0 +1,155 @@
+/*##############################################################################
+#    HPCC SYSTEMS software Copyright (C) 2012 HPCC Systems.
+#
+#    Licensed 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.
+############################################################################## */
+define([
+    "dojo/_base/declare",
+    "dojo/_base/array",
+    "dojo/store/Memory",
+    "dojo/store/Observable",
+    "dojo/store/util/QueryResults",
+
+    "hpcc/ESPUtil"
+], function (declare, arrayUtil, Memory, Observable, QueryResults,
+    ESPUtil) {
+
+    var TreeItem = declare([ESPUtil.Singleton], {
+        __hpcc_type: "none",
+
+        constructor: function (args) {
+            args.__hpcc_id = this.__hpcc_type + "::" + args.__hpcc_id;  //  args get set to "this" in base class Stateful ---
+        },
+
+        getUniqueID: function () {
+            return this.__hpcc_id;
+        },
+
+        getIcon: function () {
+            return "file.png";
+        },
+
+        getLabel: function () {
+            return "TODO";
+        }
+    });
+
+    var TreeNode = declare(null, {
+        treeSeparator: "->",
+        constructor: function (store, parentNode, treeItem) {
+            this.__hpcc_store = store;
+            if (!(parentNode === null || parentNode instanceof TreeNode)) {
+                throw "Invalid Parent Node"
+            }
+            if (parentNode) {
+                parentNode.appendChild(this);
+            }
+            this.__hpcc_treeItem = treeItem;
+            this.__hpcc_id = (this.__hpcc_parentNode ? (this.__hpcc_parentNode.getUniqueID() + this.treeSeparator) : "") + this.__hpcc_treeItem.getUniqueID();
+            this.__hpcc_children = [];
+        },
+        getUniqueID: function () {
+            return this.__hpcc_id;
+        },
+        mayHaveChildren: function () {
+            return this.__hpcc_children.length;
+        },
+        appendChild: function (child) {
+            if (!(child instanceof TreeNode)) {
+                throw "Invalid Child Node"
+            }
+            child.__hpcc_parentNode = this;
+            this.__hpcc_children.push(child);
+        },
+        appendChildren: function (children) {
+            arrayUtil.forEach(children, function (child) {
+                this.appendChild(child);
+            }, this);
+        },
+        getChildren: function (options) {
+            return this.__hpcc_children;
+        },
+        getIcon: function () {
+            return this.__hpcc_treeItem.getIcon();
+        },
+        getLabel: function () {
+            return this.__hpcc_treeItem.getLabel();
+        }
+    });
+
+    var TreeStore = declare([Memory], {
+        idProperty: "__hpcc_id",
+        treeSeparator: "->",
+
+        constructor: function (args) {
+            this.clear();
+        },
+
+        clear: function () {
+            this.cachedTreeNodes = {};
+        },
+
+        setRootNode: function (rootItem) {
+            var rootNode = this.createTreeNode(null, rootItem);
+            this.setData([rootNode]);
+            return rootNode;
+        },
+
+        createTreeNode: function (parentNode, treeItem) {
+            var retVal = new TreeNode(this, parentNode, treeItem);
+            if (this.cachedTreeNodes[retVal.getUniqueID()]) {
+                //throw retVal.getUniqueID() + " already exists.";
+            }
+            this.cachedTreeNodes[retVal.getUniqueID()] = retVal;
+            return retVal;
+        },
+
+        addItem: function (treeItem) {
+            if (this.cacheTreeItems[treeItem.getUniqueID()]) {
+                throw treeItem.getUniqueID() + " already exists.";
+            }
+            treeItem.__hpcc_store = this;
+            this.cacheTreeItems[treeItem.getUniqueID()] = treeItem;
+            return treeItem;
+        },
+
+        addChild: function (source, target) {
+            this.out_edges[source.getUniqueID()].put(this.createTreeNode(source, target));
+            this.in_edges[target.getUniqueID()].put(this.createTreeNode(target, source));
+            return target;
+        },
+
+        addChildren: function (source, targets) {
+            arrayUtil.forEach(targets, function (target) {
+                this.addChild(source, target);
+            }, this);
+        },
+
+        mayHaveChildren: function (treeNode) {
+            return treeNode.mayHaveChildren && treeNode.mayHaveChildren();
+        },
+
+        get: function (id) {
+            return this.cachedTreeNodes[id];
+        },
+
+        getChildren: function (parent, options) {
+            return parent.getChildren(options);
+        }
+    });
+
+    return {
+        Store: TreeStore,
+        Item: TreeItem
+    };
+});

+ 55 - 12
esp/src/eclwatch/ESPUtil.js

@@ -24,6 +24,8 @@ define([
     "dojo/query",
     "dojo/json",
     "dojo/aspect",
+    "dojo/Evented",
+    "dojo/on",
 
     "dijit/registry",
     "dijit/Tooltip",
@@ -36,7 +38,7 @@ define([
     "dgrid/extensions/ColumnHider",
     "dgrid/extensions/DijitRegistry",
     "dgrid/extensions/Pagination"
-], function (declare, lang, i18n, nlsHPCC, arrayUtil, domClass, Stateful, query, json, aspect,
+], function (declare, lang, i18n, nlsHPCC, arrayUtil, domClass, Stateful, query, json, aspect, Evented, on,
     registry, Tooltip,
     Grid, OnDemandGrid, Keyboard, Selection, ColumnResizer, ColumnHider, DijitRegistry, Pagination) {
 
@@ -59,8 +61,8 @@ define([
 
         //  Methods  ---
         constructor: function (args) {
-            this.changedCount = 0;
-            this._changedCache = {};
+            this.__hpcc_changedCount = 0;
+            this.__hpcc_changedCache = {};
         },
         getData: function () {
             if (this instanceof SingletonData) {
@@ -71,18 +73,20 @@ define([
         updateData: function (response) {
             var changed = false;
             for (var key in response) {
-                var jsonStr = json.stringify(response[key]);
-                if (this._changedCache[key] !== jsonStr) {
-                    this._changedCache[key] = jsonStr;
-                    this.set(key, response[key]);
-                    changed = true;
+                if (response[key] !== undefined || response[key] !== null) {
+                    var jsonStr = json.stringify(response[key]);
+                    if (this.__hpcc_changedCache[key] !== jsonStr) {
+                        this.__hpcc_changedCache[key] = jsonStr;
+                        this.set(key, response[key]);
+                        changed = true;
+                    }
                 }
             }
             if (changed) {
                 try {
-                    this.set("changedCount", this.get("changedCount") + 1);
+                    this.set("__hpcc_changedCount", this.get("__hpcc_changedCount") + 1);
                 } catch (e) {
-                    /*  changedCount can notify a dgrid instance that a row has changed.  
+                    /*  __hpcc_changedCount can notify a dgrid instance that a row has changed.  
                     *   There is an issue (TODO check issue number) with dgrid which can cause an exception to be thrown during the notify.
                     *   By catching these exceptions here normal execution can continue.
                     */
@@ -238,9 +242,48 @@ define([
         }
     });
 
+    var IdleWatcher = dojo.declare([Evented], {
+        constructor: function(idleDuration) {
+            idleDuration = idleDuration || 30 * 1000;
+            this._idleDuration = idleDuration;
+        },
+
+        start: function(){
+            this.stop();
+            var context = this;
+            this._keydownHandle = on(document, "keydown", function (item, index, array) {
+                context.stop();
+                context.start();
+            });
+            this._mousedownHandle = on(document, "mousedown", function (item, index, array) {
+                context.stop();
+                context.start();
+            });
+            this._intervalHandle = setInterval(function () {
+                context.emit("idle", {});
+            }, this._idleDuration);
+        },
+
+        stop: function(){
+            if(this._intervalHandle){
+                clearInterval(this._intervalHandle);
+                delete this._intervalHandle;
+            }
+            if (this._mousedownHandle) {
+                this._mousedownHandle.remove();
+                delete this._mousedownHandle;
+            }
+            if (this._keydownHandle) {
+                this._keydownHandle.remove();
+                delete this._keydownHandle;
+            }
+        }
+    });
+
     return {
         Singleton: SingletonData,
         Monitor: Monitor,
+        IdleWatcher: IdleWatcher,
 
         FormHelper: declare(null, {
             getISOString: function (dateField, timeField) {
@@ -258,7 +301,7 @@ define([
             }
         }),
 
-        Grid: function (pagination, selection) {
+        Grid: function (pagination, selection, overrides) {
             var baseClass = [];
             var params = {};
             if (pagination) {
@@ -281,7 +324,7 @@ define([
                 });
             }
             baseClass.push(GridHelper);
-            return declare(baseClass, params);
+            return declare(baseClass, lang.mixin(params, overrides));
         },
 
         MonitorVisibility: function (widget, callback) {

+ 3 - 3
esp/src/eclwatch/ESPWorkunit.js

@@ -59,7 +59,7 @@ define([
             storeItem.updateData(item);
             if (!this._watched[id]) {
                 var context = this;
-                this._watched[id] = storeItem.watch("changedCount", function (name, oldValue, newValue) {
+                this._watched[id] = storeItem.watch("__hpcc_changedCount", function (name, oldValue, newValue) {
                     if (oldValue !== newValue) {
                         context.notify(storeItem, id);
                     }
@@ -263,7 +263,7 @@ define([
             }
             if (!this.hasCompleted) {
                 var context = this;
-                this.watch("changedCount", function (name, oldValue, newValue) {
+                this.watch("__hpcc_changedCount", function (name, oldValue, newValue) {
                     if (oldValue !== newValue && newValue) {
                         if (callback) {
                             callback(context);
@@ -453,7 +453,7 @@ define([
             });
         },
         refresh: function (full) {
-            if (full || this.Archived || this.changedCount === 0) {
+            if (full || this.Archived || this.__hpcc_changedCount === 0) {
                 return this.getInfo({
                     onGetText: function () {
                     },

+ 8 - 0
esp/src/eclwatch/FileSpray.js

@@ -174,6 +174,14 @@ define([
         }
     });
 
+    var LogFileStore = declare([ESPRequest.Store], {
+        service: "FileSpray",
+        action: "FileList",
+        responseQualifier: "FileListResponse.files.PhysicalFileStruct",
+        idProperty: ""
+    });
+
+
     return {
         States: {
             0: "unknown",

+ 13 - 0
esp/src/eclwatch/GetDFUWorkunitsWidget.js

@@ -84,6 +84,19 @@ define([
 
         startup: function (args) {
             this.inherited(arguments);
+            this.initContextMenu();
+            this._idleWatcher = new ESPUtil.IdleWatcher();
+            this._idleWatcher.start();
+            var context = this;
+            this._idleWatcherHandle = this._idleWatcher.on("idle", function () {
+                context._onRefresh();
+            });
+        },
+
+        destroy: function (args) {
+            this._idleWatcherHandle.remove();
+            this._idleWatcher.stop();
+            this.inherited(arguments);
         },
 
         getTitle: function () {

+ 1 - 21
esp/src/eclwatch/HPCCPlatformWidget.js

@@ -105,26 +105,6 @@ define([
         },
 
         //  Implementation  ---
-        parseBuildString: function (build) {
-            if (!build) {
-                return;
-            }
-            this.build = {};
-            this.build.orig = build;
-            this.build.prefix = "";
-            this.build.postfix = "";
-            var verArray = build.split("[");
-            if (verArray.length > 1) {
-                this.build.postfix = verArray[1].split("]")[0];
-            }
-            verArray = verArray[0].split("_");
-            if (verArray.length > 1) {
-                this.build.prefix = verArray[0];
-                verArray.splice(0, 1);
-            }
-            this.build.version = verArray.join("_");
-        },
-
         refreshBanner: function (activity) {
             if (this.showBanner !== activity.ShowBanner ||
                 this.bannerContent !== activity.BannerContent ||
@@ -231,7 +211,7 @@ define([
 
             this.activity = ESPActivity.Get();
             this.activity.watch("Build", function (name, oldValue, newValue) {
-                context.parseBuildString(newValue);
+                context.build = WsSMC.parseBuildString(newValue);
             });
             this.activity.watch("changedCount", function (name, oldValue, newValue) {
                 context.refreshBanner(context.activity);

esp/src/eclwatch/LogsWidget.js → esp/src/eclwatch/HelpersWidget.js


+ 248 - 0
esp/src/eclwatch/LogWidget.js

@@ -0,0 +1,248 @@
+/*##############################################################################
+#   HPCC SYSTEMS software Copyright (C) 2012 HPCC Systems.
+#
+#   Licensed 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.
+############################################################################## */
+define([
+    "dojo/_base/declare",
+    "dojo/_base/lang",
+    "dojo/i18n",
+    "dojo/i18n!./nls/hpcc",
+    "dojo/_base/array",
+    "dojo/dom",
+    "dojo/dom-class",
+    "dojo/dom-form",
+    "dojo/date",
+    "dojo/on",
+    "dojo/topic",
+
+    "dijit/registry",
+    "dijit/Menu",
+    "dijit/MenuItem",
+    "dijit/MenuSeparator",
+    "dijit/PopupMenuItem",
+
+    "dgrid/selector",
+
+    "hpcc/_TabContainerWidget",
+    "hpcc/ESPBase",
+    "hpcc/ESPUtil",
+    "hpcc/ESPWorkunit",
+    "hpcc/DelayLoadWidget",
+    "hpcc/TargetSelectWidget",
+    "hpcc/FilterDropDownWidget",
+    "hpcc/ECLSourceWidget",
+    "hpcc/WsTopology",
+
+    "dojo/text!../templates/LogWidget.html",
+
+    "dijit/layout/BorderContainer",
+    "dijit/layout/TabContainer",
+    "dijit/layout/ContentPane",
+    "dijit/form/Textarea",
+    "dijit/form/DateTextBox",
+    "dijit/form/TimeTextBox",
+    "dijit/form/Button",
+    "dijit/form/CheckBox",
+    "dijit/form/RadioButton",
+    "dijit/form/Select",
+    "dijit/Toolbar",
+    "dijit/ToolbarSeparator",
+    "dijit/TooltipDialog"
+
+], function (declare, lang, i18n, nlsHPCC, arrayUtil, dom, domClass, domForm, date, on, topic,
+                registry, Menu, MenuItem, MenuSeparator, PopupMenuItem,
+                selector,
+                _TabContainerWidget, ESPBase, ESPUtil, ESPWorkunit, DelayLoadWidget, TargetSelectWidget, FilterDropDownWidget, ECLSourceWidget, WsTopology,
+                template) {
+    return declare("LogWidget", [_TabContainerWidget, ESPUtil.FormHelper], {
+        templateString: template,
+        baseClass: "LogWidget",
+        i18n: nlsHPCC,
+
+        logTab: null,
+        logGrid: null,
+        filter: null,
+        clusterTargetSelect: null,
+        stateSelect: null,
+
+        postCreate: function (args) {
+            this.inherited(arguments);
+            var context = this;
+
+            this.logTab = registry.byId(this.id + "_Log");
+            this.logTargetSelect = registry.byId(this.id + "logTargetSelect");
+            this.logTargetSelect.on("change", function (evt) {
+                context.refreshGrid();
+            });
+            this.reverseButton = registry.byId(this.id + "Reverse");
+            this.filter = registry.byId(this.id + "Filter");
+            this.filter.on("clear", function (evt) {
+                context._onFilterType();
+                context.refreshGrid();
+            });
+            this.filter.on("apply", function (evt) {
+                context.refreshGrid();
+            });
+            this.rawText = registry.byId(this.id + "LogText");
+        },
+
+        startup: function (args) {
+            this.inherited(arguments);
+        },
+
+        getTitle: function () {
+            return this.i18n.title_Log;
+        },
+
+        //  Hitched actions  ---
+        _onRefresh: function (event) {
+            this.refreshGrid();
+        },
+
+        _doDownload: function (zip) {
+            var base = new ESPBase();
+            var name = this.params.getLogDirectory() + "/" + this.logTargetSelect.get("value");
+            var type = "tpcomp_log";
+            window.open(base.getBaseURL("WsTopology") + "/SystemLog?Name=" + name + "&Type=" + type + "&Zip=" + zip, "_blank");
+        },
+
+        _onDownloadText: function (args) {
+            this._doDownload(1);
+        },
+
+        _onDownloadZip: function (args) {
+            this._doDownload(2);
+        },
+
+        _onDownloadGZip: function (args) {
+            this._doDownload(3);
+        },
+
+        //  Implementation  ---
+        getFilter: function () {
+            var retVal = this.filter.toObject();
+            if (retVal.FirstRows) {
+                retVal.FilterType = 1;
+            } else if (retVal.LastRows) {
+                retVal.FilterType = 5;
+            } else if (retVal.LastHours) {
+                retVal.FilterType = 2;
+            } else if (retVal.StartDate) {
+                if (retVal.StartDate[0] === "T") {
+                    retVal.StartDate = retVal.StartDate.substring(1);
+                }
+                if (retVal.EndDate[0] === "T") {
+                    retVal.EndDate = retVal.EndDate.substring(1);
+                }
+                retVal.FilterType = 6;
+            } else {
+                retVal.FilterType = 4;
+            }
+            return retVal;
+        },
+
+        //  Implementation  ---
+        init: function (params) {
+            if (this.params.__hpcc_id === params.__hpcc_id)
+                return;
+
+            this.initalized = false;
+            this.inherited(arguments);
+
+            this.logTargetSelect.reset();
+            this.logTargetSelect.init({
+                Logs: true,
+                includeBlank: false,
+                params: this.params
+            });
+
+            this.initLogGrid();
+            if (!this.rawText.initialized) {
+                this.rawText.initialized = true;
+                this.rawText.init({
+                    mode: "text"
+                });
+            }
+        },
+
+        initTab: function () {
+            var currSel = this.getSelectedChild();
+            if (currSel && !currSel.initalized) {
+                if (currSel.id == this.logTab.id) {
+                } else {
+                    if (!currSel.initalized) {
+                    }
+                }
+            }
+        },
+
+        initLogGrid: function () {
+            var context = this;
+            var store = new WsTopology.CreateTpLogFileStore();
+            store.on("pageLoaded", function (page) {
+                context.rawText.setText(page);
+            });
+            if (!this.logGrid) {
+                this.logGrid = new declare([ESPUtil.Grid(true, true, { rowsPerPage: 500, pageSizeOptions: [500, 1000] })])({
+                    store: store,
+                    query: this.getFilter(),
+                    columns: {
+                        lineNo: { label: this.i18n.Line, width: 80 },
+                        details: { label: this.i18n.Details, width: 1024 },
+                        date: { label: this.i18n.Date, width: 100 },
+                        time: { label: this.i18n.Time, width: 100 },
+                        pid: { label: "PID", width: 60 },
+                        tid: { label: "TID", width: 60 },
+                        dummy: { label: "", width: 0 }
+                    }
+                }, this.id + "LogGrid");
+
+                var context = this;
+                this.logGrid.on(".dgrid-row-url:click", function (evt) {
+                    if (context._onRowDblClick) {
+                        var item = context.logGrid.row(evt).data;
+                        context._onRowDblClick(item.Wuid);
+                    }
+                });
+                this.logGrid.on(".dgrid-row:dblclick", function (evt) {
+                    if (context._onRowDblClick) {
+                        var item = context.logGrid.row(evt).data;
+                        context._onRowDblClick(item.Wuid);
+                    }
+                });
+                this.logGrid.onSelectionChanged(function (event) {
+                    context.refreshActionState();
+                });
+                this.logGrid.startup();
+            }
+        },
+
+        refreshGrid: function (clearSelection) {
+            this.rawText.setText(this.i18n.loadingMessage);
+            var filter = lang.mixin(this.getFilter(), {
+                Name: this.params.getLogDirectory() + "/" + this.logTargetSelect.get("value"),
+                Type: "tpcomp_log",
+                LoadData: 1
+            });
+            this.logGrid.set("query", filter);
+            if (clearSelection) {
+                this.logGrid.clearSelection();
+            }
+        },
+
+        refreshActionState: function () {
+            var selection = this.logGrid.getSelected();
+        }
+    });
+});

+ 47 - 1
esp/src/eclwatch/TargetSelectWidget.js

@@ -39,6 +39,13 @@ define([
         defaultValue: "",
 
         //  Implementation  ---
+        reset: function () {
+            this.initalized = false;
+            this.loading = false;
+            this.defaultValue = "";
+            this.options = [];
+        },
+
         init: function (params) {
             if (this.initalized)
                 return;
@@ -72,6 +79,8 @@ define([
                 this.loadDFUState();
             } else if (params.ECLSamples === true) {
                 this.loadECLSamples();
+            } else if (params.Logs === true) {
+                this.loadLogs(params);
             } else {
                 this.loadTargets();
             }
@@ -109,7 +118,7 @@ define([
         },
 
         _postLoad: function () {
-            if (this.defaultValue == "") {
+            if (this.defaultValue === "" && this.options.length) {
                 this.defaultValue = this.options[0].value;
             }
             this.set("value", this.defaultValue);
@@ -273,6 +282,43 @@ define([
                 });
             });
             context._postLoad();
+        },
+
+        loadLogs: function (params) {
+            var context = this;
+            this.set("options", []);
+            FileSpray.FileList({
+                request: {
+                    Mask: "*.log",
+                    Netaddr: params.params.Netaddress,
+                    OS: params.params.OS,
+                    Path: params.params.getLogDirectory()
+                }
+            }).then(function (response) {
+                if (lang.exists("FileListResponse.files.PhysicalFileStruct", response)) {
+                    var options = [];
+                    var targetData = response.FileListResponse.files.PhysicalFileStruct;
+                    var shortestLabelLen = 9999;
+                    var shortestLabel = "";
+                    for (var i = 0; i < targetData.length; ++i) {
+                        options.push({
+                            label: targetData[i].name,// + " " + targetData[i].filesize + " " + targetData[i].modifiedtime,
+                            value: targetData[i].name
+                        });
+                        if (shortestLabelLen > targetData[i].name.length) {
+                            shortestLabelLen = targetData[i].name.length;
+                            shortestLabel = targetData[i].name;
+                        }
+                    }
+                    options.sort(function (l, r) {
+                        return -l.label.localeCompare(r.label);
+                    });
+                    context.set("options", options);
+                    context.defaultValue = shortestLabel;
+                    context._value = shortestLabel;
+                }
+                context._postLoad();
+            });
         }
     });
 });

+ 181 - 0
esp/src/eclwatch/TopologyDetailsWidget.js

@@ -0,0 +1,181 @@
+/*##############################################################################
+#   HPCC SYSTEMS software Copyright (C) 2012 HPCC Systems.
+#
+#   Licensed 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.
+############################################################################## */
+define([
+    "dojo/_base/declare",
+    "dojo/_base/lang",
+    "dojo/i18n",
+    "dojo/i18n!./nls/hpcc",
+    "dojo/dom",
+    "dojo/dom-construct",
+    "dojo/dom-form",
+    "dojo/dom-attr",
+    "dojo/request/iframe",
+    "dojo/dom-class",
+    "dojo/query",
+    "dojo/store/Memory",
+    "dojo/store/Observable",
+
+    "dijit/registry",
+
+    "dgrid/OnDemandGrid",
+    "dgrid/Keyboard",
+    "dgrid/Selection",
+    "dgrid/selector",
+    "dgrid/extensions/ColumnResizer",
+    "dgrid/extensions/DijitRegistry",
+
+    "hpcc/_TabContainerWidget",
+    "hpcc/ESPWorkunit",
+    "hpcc/ESPRequest",
+    "hpcc/TargetSelectWidget",
+    "hpcc/ECLSourceWidget",
+    "hpcc/LogWidget",
+    "hpcc/WsTopology",
+
+    "dojo/text!../templates/TopologyDetailsWidget.html",
+
+    "dijit/layout/BorderContainer",
+    "dijit/layout/TabContainer",
+    "dijit/layout/ContentPane",
+    "dijit/form/Form",
+    "dijit/form/Textarea",
+    "dijit/form/Button",
+    "dijit/form/DropDownButton",
+    "dijit/form/ValidationTextBox",
+    "dijit/Toolbar",
+    "dijit/ToolbarSeparator",
+    "dijit/TooltipDialog",
+    "dijit/TitlePane",
+    "dijit/form/TextBox",
+    "dijit/Dialog",
+    "dijit/form/SimpleTextarea",
+
+    "hpcc/TableContainer"
+], function (declare, lang, i18n, nlsHPCC, dom, domConstruct, domForm, domAttr, iframe, domClass, query, Memory, Observable,
+                registry,
+                OnDemandGrid, Keyboard, Selection, selector, ColumnResizer, DijitRegistry,
+                _TabContainerWidget, ESPWorkunit, ESPRequest, TargetSelectWidget, ECLSourceWidget, LogWidget, WsTopology,
+                template) {
+    return declare("TopologyDetailsWidget", [_TabContainerWidget], {
+        templateString: template,
+        baseClass: "TopologyDetailsWidget",
+        i18n: nlsHPCC,
+
+        summaryWidget: null,
+        configurationWidget: null,
+        configurationWidgetLoaded: false,
+        logsWidget: null,
+        logsWidgetLoaded: false,
+
+
+        postCreate: function (args) {
+            this.inherited(arguments);
+            this.details = registry.byId(this.id + "_Details");
+            this.configurationWidget = registry.byId(this.id + "_Configuration");
+            this.logsWidget = registry.byId(this.id + "_Logs");
+        },
+
+        startup: function (args) {
+            this.inherited(arguments);
+        },
+
+        destroy: function (args) {
+            this.inherited(arguments);
+        },
+
+        getTitle: function () {
+            return this.i18n.title_TopologyDetails;
+        },
+
+        //  Hitched actions  ---
+        _onRefresh: function (event) {
+        },
+
+        //  Implementation  ---
+        init: function (params) {
+            if (this.params.__hpcc_id === params.__hpcc_id)
+                return;
+
+            this.initalized = false;
+            this.widget._Summary.__hpcc_initalized = false;
+            this.widget._Configuration.__hpcc_initalized = false;
+            this.widget._Logs.__hpcc_initalized = false;
+            this.inherited(arguments);
+            if (this.params.hasConfig()) {
+                this.widget._Configuration.set("disabled", false);
+            } else {
+                this.widget._Configuration.set("disabled", true);
+                if (this.getSelectedChild().id === this.widget._Configuration.id) {
+                    this.selectChild(this.widget._Summary.id);
+                }
+            }
+            if (this.params.hasLogs()) {
+                this.widget._Logs.set("disabled", false);
+            } else {
+                this.widget._Logs.set("disabled", true);
+                if (this.getSelectedChild().id === this.widget._Logs.id) {
+                    this.selectChild(this.widget._Summary.id);
+                }
+            }
+            this.initTab();
+        },
+
+        initTab: function () {
+            var context = this;
+            var currSel = this.getSelectedChild();
+            if (currSel.id == this.widget._Summary.id && !this.widget._Summary.__hpcc_initalized) {
+                this.widget._Summary.__hpcc_initalized = true;
+                var table = domConstruct.create("table", {});
+                for (var key in this.params.__hpcc_treeItem) {
+                    if (this.params.__hpcc_treeItem.hasOwnProperty(key) && !(this.params.__hpcc_treeItem[key] instanceof Object)) {
+                        if (key.indexOf("__") !== 0) {
+                            var tr = domConstruct.create("tr", {}, table);
+                            domConstruct.create("td", {
+                                innerHTML: "<b>" + key + ":&nbsp;&nbsp;</b>"
+                            }, tr);
+                            domConstruct.create("td", {
+                                innerHTML: this.params.__hpcc_treeItem[key]
+                            }, tr);
+                        }
+                    }
+                }
+                this.details.setContent(table);
+            } else if (currSel.id === this.widget._Configuration.id && !this.widget._Configuration.__hpcc_initalized) {
+                this.widget._Configuration.__hpcc_initalized = true;
+                this.params.getConfig().then(function (response) {
+                    var xml = context.formatXml(response);
+                    context.widget._Configuration.init({
+                        sourceMode: "xml"
+                    });
+                    context.widget._Configuration.setText(xml);
+                });
+            } else if (currSel.id == this.widget._Logs.id && !this.widget._Logs.__hpcc_initalized) {
+                this.widget._Logs.__hpcc_initalized = true;
+                this.widget._Logs.init(this.params.__hpcc_treeItem);
+            }
+        },
+
+        updateInput: function (name, oldValue, newValue) {
+            var registryNode = registry.byId(this.id + name);
+            if (registryNode) {
+                registryNode.set("value", newValue);
+            }
+        },
+
+        refreshActionState: function () {
+        }
+    });
+});

+ 182 - 0
esp/src/eclwatch/TopologyWidget.js

@@ -0,0 +1,182 @@
+/*##############################################################################
+#    HPCC SYSTEMS software Copyright (C) 2012 HPCC Systems.
+#
+#    Licensed 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.
+############################################################################## */
+define([
+    "dojo/_base/declare",
+    "dojo/i18n",
+    "dojo/i18n!./nls/hpcc",
+    "dojo/on",
+
+    "dijit/registry",
+    "dijit/form/ToggleButton",
+    "dijit/ToolbarSeparator",
+    "dijit/layout/ContentPane",
+
+    "dgrid/tree",
+
+    "hpcc/GridDetailsWidget",
+    "hpcc/ESPRequest",
+    "hpcc/ESPTopology",
+    "hpcc/TopologyDetailsWidget",
+    "hpcc/DelayLoadWidget",
+    "hpcc/ESPUtil"
+
+], function (declare, i18n, nlsHPCC, on,
+                registry, ToggleButton, ToolbarSeparator, ContentPane,
+                tree,
+                GridDetailsWidget, ESPRequest, ESPTopology, TopologyDetailsWidget, DelayLoadWidget, ESPUtil) {
+    return declare("TopologyWidget", [GridDetailsWidget], {
+
+        i18n: nlsHPCC,
+        gridTitle: nlsHPCC.title_Topology,
+        idProperty: "__hpcc_id",
+
+        postCreate: function (args) {
+            this.inherited(arguments);
+            this.detailsWidget = new TopologyDetailsWidget({
+                id: this.id + "Details",
+                region: "right",
+                splitter: true,
+                style: "width: 66%",
+                minSize: 240
+            });
+            this.detailsWidget.placeAt(this.gridTab, "last");
+        },
+
+        init: function (params) {
+            if (this.inherited(arguments))
+                return;
+
+            this.refreshGrid();
+        },
+
+        createGrid: function (domID) {
+            var context = this;
+            this.openButton = registry.byId(this.id + "Open");
+            this.viewModeDebug = new ToggleButton({
+                showLabel: true,
+                checked: false,
+                style:{display: "none"},
+                onChange: function (val) {
+                    if (val) {
+                        context.viewModeMachines.set("checked", false);
+                        context.viewModeServices.set("checked", false);
+                        context.viewModeTargets.set("checked", false);
+                        context.refreshGrid("Debug");
+                    }
+                },
+                label: "Debug"
+            }).placeAt(this.openButton.domNode, "after");
+            this.viewModeMachines = new ToggleButton({
+                showLabel: true,
+                checked: false,
+                onChange: function (val) {
+                    if (val) {
+                        context.viewModeDebug.set("checked", false);
+                        context.viewModeServices.set("checked", false);
+                        context.viewModeTargets.set("checked", false);
+                        context.refreshGrid("Machines");
+                    }
+                },
+                label: "Machines"
+            }).placeAt(this.openButton.domNode, "after");
+            this.viewModeServices = new ToggleButton({
+                showLabel: true,
+                checked: false,
+                onChange: function (val) {
+                    if (val) {
+                        context.viewModeDebug.set("checked", false);
+                        context.viewModeMachines.set("checked", false);
+                        context.viewModeTargets.set("checked", false);
+                        context.refreshGrid("Services");
+                    }
+                },
+                label: "Services"
+            }).placeAt(this.openButton.domNode, "after");
+            this.viewModeTargets = new ToggleButton({
+                showLabel: true,
+                checked: true,
+                onChange: function (val) {
+                    if (val) {
+                        context.viewModeDebug.set("checked", false);
+                        context.viewModeMachines.set("checked", false);
+                        context.viewModeServices.set("checked", false);
+                        context.refreshGrid("Targets");
+                    }
+                },
+                label: "Targets"
+            }).placeAt(this.openButton.domNode, "after");
+            new ToolbarSeparator().placeAt(this.openButton.domNode, "after");
+
+            this.store = new ESPTopology.Store();
+            var retVal = new declare([ESPUtil.Grid(false, true)])({
+                store: this.store,
+                columns: [
+                tree({
+                        field: "__hpcc_displayName",
+                        label: this.i18n.Topology,
+                        width: 150,
+                        collapseOnRefresh: true,
+                        shouldExpand: function (row, level, previouslyExpanded) {
+                            if (previouslyExpanded !== undefined) {
+                                return previouslyExpanded;
+                            } else if (level < -1) {
+                                return true;
+                            }
+                            return false;
+                        },
+                        formatter: function (_id, row) {
+                            return "<img src='" + dojoConfig.getImageURL(row.getIcon()) + "'/>&nbsp;" + row.getLabel();
+                        }
+                    })
+                ]
+            }, domID);
+
+            retVal.on("dgrid-select", function (event) {
+                var selection = context.grid.getSelected();
+                if (selection.length) {
+                    context.detailsWidget.init(selection[0]);
+                }
+            });
+
+            return retVal;
+        },
+
+        createDetail: function (id, row, params) {
+            return new DelayLoadWidget({
+                id: id,
+                title: row.__hpcc_displayName,
+                closable: true,
+                delayWidget: "TopologyDetailsWidget",
+                hpcc: {
+                    params: row
+                }
+            });
+        },
+
+        refreshGrid: function (mode) {
+            var context = this;
+            if (mode) {
+                this.store.viewMode(mode);
+                this.grid.refresh();
+            } else {
+                this.store.viewMode("Targets");
+                this.store.refresh(function () {
+                    context.grid.refresh();
+                });
+            }
+        }
+    });
+});

+ 1 - 1
esp/src/eclwatch/WUDetailsWidget.js

@@ -437,7 +437,7 @@ define([
                 domClass.remove("scopeOptional", "hidden");
                 domClass.add("scopeOptional", "show");
             }
-            if (name === "changedCount" && newValue > 0) {
+            if (name === "__hpcc_changedCount" && newValue > 0) {
                 var getInt = function (item) {
                     if (item)
                         return item;

+ 12 - 0
esp/src/eclwatch/WUQueryWidget.js

@@ -84,6 +84,18 @@ define([
         startup: function (args) {
             this.inherited(arguments);
             this.initContextMenu();
+            this._idleWatcher = new ESPUtil.IdleWatcher();
+            this._idleWatcher.start();
+            var context = this;
+            this._idleWatcherHandle = this._idleWatcher.on("idle", function () {
+                context._onRefresh();
+            });
+        },
+
+        destroy: function (args) {
+            this._idleWatcherHandle.remove();
+            this._idleWatcher.stop();
+            this.inherited(arguments);
         },
 
         getTitle: function () {

+ 25 - 0
esp/src/eclwatch/WsSMC.js

@@ -51,6 +51,31 @@ define([
         },
         MoveJobBack: function (params) {
             return ESPRequest.send("WsSMC", "MoveJobBack", params);
+        },
+        parseBuildString: function (build) {
+            var retVal = {
+                orig: build,
+                prefix: "",
+                postfix: "",
+                version: ""
+            };
+            if (!build) {
+                return retVal;
+            }
+            retVal.orig = build;
+            retVal.prefix = "";
+            retVal.postfix = "";
+            var verArray = build.split("[");
+            if (verArray.length > 1) {
+                retVal.postfix = verArray[1].split("]")[0];
+            }
+            verArray = verArray[0].split("_");
+            if (verArray.length > 1) {
+                retVal.prefix = verArray[0];
+                verArray.splice(0, 1);
+            }
+            retVal.version = verArray.join("_");
+            return retVal;
         }
     };
 });

+ 104 - 2
esp/src/eclwatch/WsTopology.js

@@ -18,17 +18,104 @@ define([
     "dojo/_base/lang",
     "dojo/_base/array",
     "dojo/_base/Deferred",
+    "dojo/store/Memory",
+    "dojo/store/Observable",
+    "dojo/store/util/QueryResults",
+    "dojo/Evented",
 
     "hpcc/ESPRequest"
-], function (declare, lang, arrayUtil, Deferred,
+], function (declare, lang, arrayUtil, Deferred, Memory, Observable, QueryResults, Evented,
     ESPRequest) {
-    return {
+
+    var TpLogFileStore = declare([Memory, Evented], {
+        constructor: function () {
+            this.idProperty = "__hpcc_id";
+        },
+
+        query: function (query, options) {
+            var deferredResults = new Deferred();
+            deferredResults.total = new Deferred();
+            if (!query.Name) {
+                deferredResults.resolve([]);
+                deferredResults.total.resolve(0);
+            } else {
+                var results = self.TpLogFile({
+                    request: lang.mixin({}, query, {
+                        PageNumber: options.start / options.count
+                    })
+                }).then(lang.hitch(this, function (response) {
+                    var data = [];
+                    if (lang.exists("TpLogFileResponse.LogData", response)) {
+                        this.lastPage = response.TpLogFileResponse.LogData;
+                        this.emit("pageLoaded", this.lastPage);
+                        arrayUtil.forEach(response.TpLogFileResponse.LogData.split("\n"), function (item, idx) {
+                            if (options.start === 0 || idx > 0) {
+                                //  Throw away first line as it will probably only be a partial line  ---
+                                function nextItem(itemParts) {
+                                    var part = "";
+                                    while (itemParts.length && part.trim() === "") {
+                                        part = itemParts[0]; itemParts.shift();
+                                    }
+                                    return part;
+                                }
+                                var itemParts = item.split(" ");
+                                var lineNo, date, time, pid, tid, details;
+                                if (itemParts.length) lineNo = nextItem(itemParts);
+                                if (itemParts.length) date = nextItem(itemParts);
+                                if (itemParts.length) time = nextItem(itemParts);
+                                if (itemParts.length) pid = nextItem(itemParts);
+                                if (itemParts.length) tid = nextItem(itemParts);
+                                if (itemParts.length) details = itemParts.join(" ");
+                                data.push({
+                                    __hpcc_id: response.TpLogFileResponse.PageNumber + "_" + idx,
+                                    lineNo: lineNo,
+                                    date: date,
+                                    time: time,
+                                    pid: pid,
+                                    tid: tid,
+                                    details: details
+                                });
+                            }
+                        }, this);
+                    }
+                    this.setData(data);
+                    if (lang.exists("TpLogFileResponse.TotalPages", response)) {
+                        deferredResults.total.resolve(response.TpLogFileResponse.TotalPages * options.count);
+                    } else {
+                        deferredResults.total.resolve(data.length);
+                    }
+                    return deferredResults.resolve(this.data);
+                }));
+            }
+            return QueryResults(deferredResults);
+        }
+    });
+
+    var TpLogFileStoreXXX = declare([ESPRequest.Store], {
+        service: "WsTopology",
+        action: "TpLogFile",
+        responseQualifier: "TpLogFileResponse.LogData",
+        responseTotalQualifier: "TpLogFileResponse.TotalPages",
+        idProperty: "Wuid",
+        startProperty: "PageStartFrom",
+        countProperty: "Count"
+    });
+
+    var self = {
         TpServiceQuery: function (params) {
             lang.mixin(params.request, {
                 Type: "ALLSERVICES"
             });
             return ESPRequest.send("WsTopology", "TpServiceQuery", params);
         },
+
+        TpClusterQuery: function (params) {
+            lang.mixin(params.request, {
+                Type: "ROOT"
+            });
+            return ESPRequest.send("WsTopology", "TpClusterQuery", params);
+        },
+
         GetESPServiceBaseURL: function (type) {
             var deferred = new Deferred();
             var context = this;
@@ -124,6 +211,21 @@ define([
         },
         TpGetServicePlugins: function (params) {
             return ESPRequest.send("WsTopology", "TpGetServicePlugins", params);
+        },
+        TpGetComponentFile: function (params) {
+            params.handleAs = "text";
+            if (params.request.CompType === "EclAgentProcess") {
+                params.request.CompType = "AgentExecProcess";
+            }
+            return ESPRequest.send("WsTopology", "TpGetComponentFile", params);
+        },
+        TpLogFile: function (params) {
+            return ESPRequest.send("WsTopology", "TpLogFile", params);
+        },
+        CreateTpLogFileStore: function () {
+            var store = new TpLogFileStore();
+            return Observable(store);
         }
     };
+    return self;
 });

+ 1 - 1
esp/src/eclwatch/_Widget.js

@@ -210,7 +210,7 @@ define([
                 var ln = lines[i];
                 var single = Boolean(ln.match(/<.+\/>/)); // is this line a single tag? ex. <br />
                 var closing = Boolean(ln.match(/<\/.+>/)); // is this a closing tag? ex. </a>
-                var opening = Boolean(ln.match(/<[^!].*>/)); // is this even a tag (that's not <!something>)
+                var opening = Boolean(ln.match(/<[^!?].*>/)); // is this even a tag (that's not <!something>)
                 var type = single ? 'single' : closing ? 'closing' : opening ? 'opening' : 'other';
                 var fromTo = lastType + '->' + type;
                 lastType = type;

BIN
esp/src/eclwatch/img/cluster.png


BIN
esp/src/eclwatch/img/machine.png


+ 13 - 0
esp/src/eclwatch/nls/hpcc.js

@@ -169,12 +169,14 @@ define({root:
     FindNext: "Find Next",
     FindPrevious: "Find Previous",
     FirstName: "First Name",
+    FirstNRows: "First N Rows",
     Fixed: "Fixed",
     Folder: "Folder",
     Format: "Format",
     Forward: "Forward",
     FromDate: "From Date",
     FromSizes: "From Sizes",
+    FromTime: "From Time",
     FullName: "Full Name",
     Graph: "Graph",
     Graphs: "Graphs",
@@ -217,6 +219,8 @@ define({root:
     LargestSize: "Largest Size",
     LastName: "Last Name",
     LastNDays: "Last N Days",
+    LastNHours: "Last N Hours",
+    LastNRows: "Last N Rows",
     LegacyForm: "Legacy Form",
     LDAPWarning: "<b>LDAP Services Error:</b>  'Too Many Users' - Please use a Filter.",
     LibrariesUsed: "Libraries Used",
@@ -239,6 +243,8 @@ define({root:
     LogicalFilesOnly: "Logical Files Only",
     LogicalFileType: "Logical File Type",
     LogicalName: "Logical Name",
+    Log: "Log",
+    Logs: "Logs",
     log_analysis_1: "log_analysis_1*",
     Low: "Low",
     ManualCopy: "Press Ctrl+C",
@@ -364,6 +370,7 @@ define({root:
     Queue: "Queue",
     Quote: "Quote",
     QuotedTerminator: "Quoted Terminator",
+    RawTextPage: "Raw Text (Current Page)",
     RecordCount: "Record Count",
     RecordLength: "Record Length",
     Records: "Records",
@@ -401,6 +408,7 @@ define({root:
     Resume: "Resume",
     RetainSuperfileStructure: "Retain Superfile Structure",
     RetypePassword: "Retype Password",
+    Reverse: "Reverse",
     Rows: "Rows",
     RowPath: "Row Path",
     RowTag: "Row Tag",
@@ -499,6 +507,7 @@ define({root:
     title_HPCCPlatformRoxie: "ECL Watch - Roxie",
     title_HPCCPlatformServicesPlugin: "ECL Watch - Plugins",
     title_Inputs: "Inputs",
+    title_Log: "Log File",
     title_LFDetails: "Logical File Details",
     title_LZBrowse: "Landing Zones",
     title_MemberOf: "Member Of",
@@ -514,18 +523,22 @@ define({root:
     title_Results: "Outputs",
     title_SearchResults: "Search Results",
     title_SourceFiles: "",
+    title_Topology: "Topology",
     title_TpThorStatus: "Thor Status",
     title_UserQuery: "Permissions",
     title_UserPermissions: "User Permissions",
     title_WUDetails: "ECL Workunit Details",
     title_WUQuery: "ECL Workunits",
+    To: "To",
     ToDate: "To Date",
     Toenablegraphviews: "To enable graph views, please install the Graph View Control plugin",
     Top: "Top",
+    Topology: "Topology",
     ToSizes: "To Sizes",
     TotalSize: "Total Size",
     TotalClusterTime: "Total Cluster Time",
     TransitionGuide: "Transition Guide",
+    Text: "Text",
     Tree: "Tree",
     Type: "Type",
     undefined: "undefined",

+ 3 - 3
esp/src/eclwatch/templates/CurrentUserDetailsWidget.html

@@ -23,16 +23,16 @@
                     </li>
                     <li>
                         <label class="Prompt" for="${id}OldPassword">${i18n.OldPassword}:</label>
-                        <input name="oldPassword" type="password" data-dojo-type="dijit.form.ValidationTextBox" />
+                        <input name="oldPassword" type="password" required="true" data-dojo-type="dijit.form.ValidationTextBox" />
                     </li>
                     <div name="newPassword" data-dojo-props="required: false" data-dojo-type="dojox.form.PasswordValidator">
                         <li>
                             <label class="Prompt" for="${id}NewPassword">${i18n.NewPassword}:</label>
-                            <input name="newPassword" type="password" pwtype="new" data-dojo-props="invalidMessage:'${i18n.PasswordsDoNotMatch}', placeHolder:'${i18n.MustContainUppercaseAndSymbol}'" data-dojo-type="dijit.form.ValidationTextBox" />
+                            <input name="newPassword" type="password" required="true" pwtype="new" data-dojo-props="invalidMessage:'${i18n.PasswordsDoNotMatch}', placeHolder:'${i18n.MustContainUppercaseAndSymbol}'" data-dojo-type="dijit.form.ValidationTextBox" />
                         </li>
                         <li>
                             <label class="Prompt" for="${id}VerifyPassword">${i18n.ConfirmPassword}:</label>
-                            <input name="newPasswordRetype" type="password" pwtype="verify" data-dojo-props="invalidMessage:'${i18n.PasswordsDoNotMatch}', placeHolder:'${i18n.MustContainUppercaseAndSymbol}'" data-dojo-type="dijit.form.ValidationTextBox" />
+                            <input name="newPasswordRetype" type="password" required="true" pwtype="verify" data-dojo-props="invalidMessage:'${i18n.PasswordsDoNotMatch}', placeHolder:'${i18n.MustContainUppercaseAndSymbol}'" data-dojo-type="dijit.form.ValidationTextBox" />
                         </li>
                     </div>
                     <li>

+ 2 - 0
esp/src/eclwatch/templates/HPCCPlatformOpsWidget.html

@@ -4,6 +4,8 @@
             <div id="${id}StackController" style="width: 100%" data-dojo-props="containerId:'${id}TabContainer'" data-dojo-type="dijit.layout.StackController"></div>
         </div>
         <div id="${id}TabContainer" data-dojo-props="region: 'center', tabPosition: 'top'" style="width: 100%; height: 100%" data-dojo-type="dijit.layout.StackContainer">
+            <div id="${id}_Topology" title="${i18n.Topology}" style="padding: 0px; border:0px; border-color:none; overflow: hidden" data-dojo-props="delayWidget: 'TopologyWidget'" data-dojo-type="DelayLoadWidget">
+            </div>
             <div id="${id}_DiskUsage" title="${i18n.DiskUsage}" style="padding: 0px; border:0px; border-color:none; overflow: hidden" data-dojo-props="delayWidget: 'DiskUsageWidget'" data-dojo-type="DelayLoadWidget">
             </div>
             <div id="${id}_TargetClusters" title="${i18n.TargetClusters}" style="padding: 0px; border:0px; border-color:none; overflow: hidden" data-dojo-type="dijit.layout.ContentPane">

+ 35 - 0
esp/src/eclwatch/templates/LogWidget.html

@@ -0,0 +1,35 @@
+<div class="${baseClass}">
+    <div id="${id}BorderContainer" class="${baseClass}BorderContainer" style="width: 100%; height: 100%;" data-dojo-type="dijit.layout.BorderContainer">
+        <div id="${id}Toolbar" class="topPanel" data-dojo-props="region: 'top'" data-dojo-type="dijit.Toolbar">
+            <div id="${id}Refresh" data-dojo-attach-event="onClick:_onRefresh" data-dojo-props="iconClass:'iconRefresh'" data-dojo-type="dijit.form.Button">${i18n.Refresh}</div>
+            <input id="${id}logTargetSelect" title="${i18n.Log}:" name="Log" style="width: 300px" data-dojo-props="trim: true" data-dojo-type="TargetSelectWidget" />
+            <span data-dojo-type="dijit.ToolbarSeparator"></span>
+            <b>${i18n.Download}:</b>
+            <div data-dojo-attach-event="onClick:_onDownloadText" data-dojo-type="dijit.form.Button">${i18n.Text}</div>
+            <div data-dojo-attach-event="onClick:_onDownloadZip" data-dojo-type="dijit.form.Button">${i18n.Zip}</div>
+            <div data-dojo-attach-event="onClick:_onDownloadGZip" data-dojo-type="dijit.form.Button">${i18n.GZip}</div>
+            <span data-dojo-type="dijit.ToolbarSeparator"></span>
+            <div id="${id}Filter" data-dojo-type="FilterDropDownWidget">
+                <input id="${id}FirstNRows" title="${i18n.FirstNRows}:" name="FirstRows" colspan="1" data-dojo-props="dtrim: true, placeHolder:'300'" data-dojo-type="dijit.form.TextBox" />
+                <input id="${id}LastNRows" title="${i18n.LastNRows}:" name="LastRows" colspan="2" data-dojo-props="dtrim: true, placeHolder:'300'" data-dojo-type="dijit.form.TextBox" />
+                <input id="${id}FromTime" title="${i18n.FromTime}:" name="StartDate" data-dojo-props="trim: true, placeHolder:'7:30 AM'" data-dojo-type="dijit.form.TimeTextBox" />
+                <input id="${id}ToTime" title="${i18n.To}:" name="EndDate" data-dojo-props="trim: true, placeHolder:'7:30 PM'" data-dojo-type="dijit.form.TimeTextBox" />
+                <input id="${id}LastNHours" title="${i18n.LastNHours}:" name="LastHours" data-dojo-props="dtrim: true, placeHolder:'2'" data-dojo-type="dijit.form.TextBox" />
+            </div>
+            <span data-dojo-type="dijit.ToolbarSeparator"></span>
+            <div id="${id}NewPage" class="right" data-dojo-attach-event="onClick:_onNewPage" data-dojo-props="iconClass:'iconNewPage', showLabel:false" data-dojo-type="dijit.form.Button">${i18n.OpenInNewPage}</div>
+        </div>
+        <div id="${id}TabContainer" data-dojo-props="region: 'center', tabPosition: 'top'" style="width: 100%; height: 100%" data-dojo-type="dijit.layout.TabContainer">
+            <div id="${id}_Log" style="width: 100%; height: 100%" data-dojo-props="title:'${i18n.Log}'" data-dojo-type="dijit.layout.BorderContainer">
+                <div id="${id}LogGridCP" style="border:0px; padding: 0px; border-color:none" data-dojo-props="region: 'center'" data-dojo-type="dijit.layout.ContentPane">
+                    <div id="${id}LogGrid">
+                    </div>
+                </div>
+            </div>
+            <div id="${id}_Raw" style="width: 100%; height: 100%" data-dojo-props="title:'${i18n.RawTextPage}'" data-dojo-type="dijit.layout.BorderContainer">
+                <div id="${id}LogText" style="border:0px; padding: 0px; border-color:none" data-dojo-props="region: 'center'" data-dojo-type="ECLSourceWidget">
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 20 - 0
esp/src/eclwatch/templates/TopologyDetailsWidget.html

@@ -0,0 +1,20 @@
+<div class="${baseClass}">
+    <div id="${id}BorderContainer" class="${baseClass}BorderContainer" style="width: 100%; height: 100%;" data-dojo-type="dijit.layout.BorderContainer">
+        <div id="${id}TabContainer" data-dojo-props="region: 'center', tabPosition: 'top'" style="width: 100%; height: 100%" data-dojo-type="dijit.layout.TabContainer">
+            <div id="${id}_Summary" style="width: 100%; height: 100%" data-dojo-props="title:'${i18n.Summary}', iconClass:'iconWorkunit'" data-dojo-type="dijit.layout.BorderContainer">
+                <div id="${id}Toolbar" class="topPanel" data-dojo-props="region: 'top'" data-dojo-type="dijit.Toolbar">
+                    <div id="${id}Refresh" data-dojo-attach-event="onClick:_onRefresh" data-dojo-props="iconClass:'iconRefresh'" data-dojo-type="dijit.form.Button">${i18n.Refresh}</div>
+                    <span data-dojo-type="dijit.ToolbarSeparator"></span>
+                    <div id="${id}NewPage" class="right" data-dojo-attach-event="onClick:_onNewPage" data-dojo-props="iconClass:'iconNewPage', showLabel:false" data-dojo-type="dijit.form.Button">${i18n.OpenInNewPage}</div>
+                </div>
+                <div id="${id}_Details" data-dojo-props="region: 'center'" data-dojo-type="dijit.layout.ContentPane">
+                    TODO
+                </div>
+            </div>
+            <div id="${id}_Configuration" title="${i18n.Configuration}" data-dojo-type="ECLSourceWidget">
+            </div>
+            <div id="${id}_Logs" title="${i18n.Logs}" data-dojo-type="LogWidget">
+            </div>
+        </div>
+    </div>
+</div>

+ 2 - 2
esp/src/eclwatch/templates/WUDetailsWidget.html

@@ -109,7 +109,7 @@
             </div>
             <div id="${id}_Resources" title="${i18n.Resources}" data-dojo-props="delayWidget: 'ResourcesWidget', disabled: true" data-dojo-type="DelayLoadWidget">
             </div>
-            <div id="${id}_Logs" title="${i18n.Helpers}" data-dojo-props="delayWidget: 'LogsWidget', disabled: true" data-dojo-type="DelayLoadWidget">
+            <div id="${id}_Logs" title="${i18n.Helpers}" data-dojo-props="delayWidget: 'HelpersWidget', disabled: true" data-dojo-type="DelayLoadWidget">
             </div>
             <div id="${id}_ECL" title="${i18n.ECL}" data-dojo-props="delayWidget: 'ECLSourceWidget'" data-dojo-type="DelayLoadWidget">
             </div>
@@ -133,4 +133,4 @@
             <button class="bottomFormButtons" data-dojo-attach-event="onClick:_onCancelDialog" data-dojo-type="dijit.form.Button">${i18n.Cancel}</button>
         </form>
     </div>
-</div>
+</div>

+ 12 - 9
system/jlib/jcomp.cpp

@@ -468,9 +468,6 @@ bool CppCompiler::compileFile(IThreadPool * pool, const char * filename, Semapho
         return false;
 
     StringBuffer cmdline;
-    StringBuffer basename;
-    splitFilename(filename, &basename, &basename, &basename, NULL);
-
     cmdline.append(CC_NAME[targetCompiler]);
     if (precompileHeader)
         cmdline.append(CC_OPTION_PRECOMPILEHEADER[targetCompiler]);
@@ -495,22 +492,25 @@ bool CppCompiler::compileFile(IThreadPool * pool, const char * filename, Semapho
     {
         if (targetDir.get())
             cmdline.append(" /Fo").append("\"").append(targetDir).append("\"");
-        cmdline.append(" /Fd").append("\"").append(targetDir).append(createDLL ? SharedObjectPrefix : NULL).append(basename).append(".pdb").append("\"");//MORE: prefer create a single pdb file using coreName
+
+        StringBuffer basename;
+        splitFilename(filename, &basename, &basename, &basename, NULL);
+        cmdline.append(" /Fd").append("\"").append(targetDir).append(createDLL ? SharedObjectPrefix : NULL).append(filename).append(".pdb").append("\"");//MORE: prefer create a single pdb file using coreName
     }
     else
     {
-        cmdline.append(" -o ").append("\"").append(targetDir).append(basename).append('.');
+        cmdline.append(" -o ").append("\"");
         if (precompileHeader)
-            cmdline.append(PCH_FILE_EXT[targetCompiler]);
+            cmdline.append(targetDir).append(filename).append('.').append(PCH_FILE_EXT[targetCompiler]);
         else
-            cmdline.append(OBJECT_FILE_EXT[targetCompiler]);
+            getObjectName(cmdline, filename);
         cmdline.append("\"");
     }
     
     StringBuffer expanded;
     expandRootDirectory(expanded, cmdline);
     StringBuffer logFile;
-    logFile.append(basename).append(".log.tmp");
+    logFile.append(filename).append(".log.tmp");
     logFiles.append(logFile);
 
     Owned<CCompilerThreadParam> parm;
@@ -713,7 +713,10 @@ void CppCompiler::expandRootDirectory(StringBuffer & expanded, StringBuffer & in
 StringBuffer & CppCompiler::getObjectName(StringBuffer & out, const char * filename)
 {
     out.append(targetDir);
-    splitFilename(filename, NULL, NULL, &out, NULL);
+    if (targetCompiler == Vs6CppCompiler)
+        splitFilename(filename, NULL, NULL, &out, NULL);
+    else
+        splitFilename(filename, NULL, NULL, &out, &out);
     return out.append(".").append(OBJECT_FILE_EXT[targetCompiler]);
 }
 

+ 2 - 0
testing/regress/ecl/failrestart.ecl

@@ -15,6 +15,8 @@
     limitations under the License.
 ############################################################################## */
 
+//fail
+
 //The first time this workunit executes it gets an exception in a conditional workflow item.
 //If it is "recovered" the condition must be re-evaluated (otherwise the false branch is taken the
 //second time.

+ 73 - 0
testing/regress/ecl/key/sort3.xml

@@ -0,0 +1,73 @@
+<Dataset name='Result 1'>
+ <Row><uv>1</uv><sv>GAVIN     </sv></Row>
+ <Row><uv>1</uv><sv>CHARLIE   </sv></Row>
+ <Row><uv>513</uv><sv>Jim       </sv></Row>
+ <Row><uv>769</uv><sv>JAMES     </sv></Row>
+ <Row><uv>769</uv><sv>ABSALOM   </sv></Row>
+ <Row><uv>769</uv><sv>BARNEY    </sv></Row>
+ <Row><uv>769</uv><sv>CHARLIE   </sv></Row>
+ <Row><uv>769</uv><sv>JETHROW   </sv></Row>
+ <Row><uv>769</uv><sv>DANIEL    </sv></Row>
+</Dataset>
+<Dataset name='Result 2'>
+ <Row><uv>1</uv><sv>GAVIN     </sv></Row>
+ <Row><uv>769</uv><sv>JAMES     </sv></Row>
+ <Row><uv>769</uv><sv>ABSALOM   </sv></Row>
+ <Row><uv>769</uv><sv>BARNEY    </sv></Row>
+ <Row><uv>769</uv><sv>CHARLIE   </sv></Row>
+ <Row><uv>769</uv><sv>JETHROW   </sv></Row>
+ <Row><uv>1</uv><sv>CHARLIE   </sv></Row>
+ <Row><uv>769</uv><sv>DANIEL    </sv></Row>
+ <Row><uv>513</uv><sv>Jim       </sv></Row>
+</Dataset>
+<Dataset name='Result 3'>
+ <Row><uv>769</uv><sv>ABSALOM   </sv></Row>
+ <Row><uv>769</uv><sv>BARNEY    </sv></Row>
+ <Row><uv>769</uv><sv>CHARLIE   </sv></Row>
+ <Row><uv>1</uv><sv>CHARLIE   </sv></Row>
+ <Row><uv>769</uv><sv>DANIEL    </sv></Row>
+ <Row><uv>1</uv><sv>GAVIN     </sv></Row>
+ <Row><uv>769</uv><sv>JAMES     </sv></Row>
+ <Row><uv>769</uv><sv>JETHROW   </sv></Row>
+ <Row><uv>513</uv><sv>Jim       </sv></Row>
+</Dataset>
+<Dataset name='Result 4'>
+ <Row><uv>769</uv><sv>ABSALOM   </sv></Row>
+ <Row><uv>769</uv><sv>BARNEY    </sv></Row>
+ <Row><uv>1</uv><sv>CHARLIE   </sv></Row>
+ <Row><uv>769</uv><sv>CHARLIE   </sv></Row>
+ <Row><uv>769</uv><sv>DANIEL    </sv></Row>
+ <Row><uv>1</uv><sv>GAVIN     </sv></Row>
+ <Row><uv>769</uv><sv>JAMES     </sv></Row>
+ <Row><uv>769</uv><sv>JETHROW   </sv></Row>
+ <Row><uv>513</uv><sv>Jim       </sv></Row>
+</Dataset>
+<Dataset name='Result 5'>
+ <Row><uv>769</uv><sv>ABSALOM   </sv></Row>
+ <Row><uv>769</uv><sv>BARNEY    </sv></Row>
+ <Row><uv>1</uv><sv>CHARLIE   </sv></Row>
+ <Row><uv>769</uv><sv>CHARLIE   </sv></Row>
+ <Row><uv>769</uv><sv>DANIEL    </sv></Row>
+ <Row><uv>1</uv><sv>GAVIN     </sv></Row>
+ <Row><uv>769</uv><sv>JAMES     </sv></Row>
+ <Row><uv>769</uv><sv>JETHROW   </sv></Row>
+ <Row><uv>513</uv><sv>Jim       </sv></Row>
+</Dataset>
+<Dataset name='Result 6'>
+ <Row><uv>769</uv><sv>ABSALOM   </sv></Row>
+ <Row><uv>769</uv><sv>BARNEY    </sv></Row>
+ <Row><uv>1</uv><sv>CHARLIE   </sv></Row>
+ <Row><uv>769</uv><sv>CHARLIE   </sv></Row>
+ <Row><uv>769</uv><sv>DANIEL    </sv></Row>
+ <Row><uv>1</uv><sv>GAVIN     </sv></Row>
+ <Row><uv>769</uv><sv>JAMES     </sv></Row>
+ <Row><uv>769</uv><sv>JETHROW   </sv></Row>
+ <Row><uv>513</uv><sv>Jim       </sv></Row>
+</Dataset>
+<Dataset name='Result 7'>
+</Dataset>
+<Dataset name='Result 8'>
+</Dataset>
+<Dataset name='Result 9'>
+ <Row><Result_9>done</Result_9></Row>
+</Dataset>

+ 2 - 0
testing/regress/ecl/layouttrans_disabled.ecl

@@ -15,6 +15,8 @@
     limitations under the License.
 ############################################################################## */
 
+//fail
+
 //class=error
 
 import ^ as root;

+ 50 - 0
testing/regress/ecl/sort3.ecl

@@ -0,0 +1,50 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2015 HPCC Systems.
+
+    Licensed 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.
+############################################################################## */
+
+r1 := { unsigned id };
+MyRec := RECORD
+    UNSIGNED2 uv;
+    STRING10   sv;
+END;
+MyRec2 := RECORD(myRec)
+    DATASET(r1) child;
+END;
+
+SomeFile := DATASET([{0x001,'GAVIN'},
+                     {0x301,'JAMES'},
+                     {0x301,'ABSALOM'},
+                     {0x301,'BARNEY'},
+                     {0x301,'CHARLIE'},
+                     {0x301,'JETHROW'},
+                     {0x001,'CHARLIE'},
+                     {0x301,'DANIEL'},
+                     {0x201,'Jim'}
+                    ],MyRec);
+
+p := PROJECT(SomeFile, TRANSFORM(myRec2, SELF := LEFT; SELF := []));
+
+sequential(
+    output(SORT(SomeFile, uv)), // needs to be stable
+    output(SORT(SomeFile, (unsigned1)uv)), // needs to be stable
+    output(SORT(SomeFile, (unsigned1)uv,sv)), // needs to be stable
+    output(SORT(SomeFile, (unsigned1)uv,sv,uv)), // can be unstable
+    output(SORT(SomeFile, trim(sv),uv)), // can be unstable
+    output(SORT(SomeFile, (string20)sv,(unsigned4)uv)), // can be unstable
+    buildindex(NOFOLD(SomeFile), { uv }, { SomeFile }, 'REGRESS:dummyIndex1',overwrite);
+    buildindex(NOFOLD(p), { uv }, { p }, 'REGRESS:dummyIndex2',overwrite);
+    output('done')
+);    

+ 2 - 0
testing/regress/ecl/superfile6.ecl

@@ -15,6 +15,8 @@
     limitations under the License.
 ############################################################################## */
 
+//fail
+
 import Std.File AS FileServices;
 // Super File added to itself test