the experimental hierarchical tree for rally - rally

I see that Hierarchical trees are labeled as experimental on the Rally site (https://help.rallydev.com/apps/2.0rc3/doc/#!/api/Rally.ui.grid.TreeGrid). I wanted to build an app using the hierarchical tree and I had a few questions about the features. Is it possible to filter the tree or no? Also can i add up the totals of the tasks for a given userstory (estimate, todo, actual, etc) and list that total as the userstory value? Is there another way to get a list of the userstories with the tasks in a list beneath it?

A not-treegrid example: this app that uses group and summary features in a grid of tasks in current iteration grouped by workproduct (user story), where Estimate values of individual tasks are summed up. Full code is in this github repo.
launch: function() {
var that = this;
var today = new Date().toISOString();
var stories = Ext.create('Rally.data.wsapi.Store', {
model: 'UserStory',
fetch: ['Tasks'],
filters: [
{
property: 'Iteration.StartDate',
operator: '<=',
value: today
},
{
property: 'Iteration.EndDate',
operator: '>=',
value: today
}
]
});
stories.load().then({
success: this.loadTasks,
scope: this
}).then({
success:function(results) {
that.makeGrid(results);
},
failure: function(){
console.log("oh noes!")
}
});
},
loadTasks: function(stories){
console.log("load tasks",stories)
var promises = [];
_.each(stories, function(story){
var tasks = story.get('Tasks');
if (tasks.Count > 0) {
tasks.store = story.getCollection('Tasks',{fetch:['Name','FormattedID','Estimate','State','Blocked','WorkProduct']});
promises.push(tasks.store.load());
}
});
return Deft.Promise.all(promises);
},
makeGrid: function(results){
var tasks = _.flatten(results);
var data = [];
_.each(tasks, function(task){
data.push(task.data);
})
_.each(data, function(record){
record.Story = record.WorkProduct.FormattedID + " " + record.WorkProduct.Name;;
})
this.add({
xtype: 'rallygrid',
showPagingToolbar: true,
showRowActionsColumn: true,
editable: false,
store: Ext.create('Rally.data.custom.Store', {
data: data,
groupField: 'Story',
}),
features: [{ftype:'groupingsummary'}],
columnCfgs: [
{
xtype: 'templatecolumn',text: 'ID',dataIndex: 'FormattedID',width: 100,
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate'),
summaryRenderer: function() {
return "Estimate Total";
}
},
{
text: 'Name',dataIndex: 'Name',
},
{
text: 'State',dataIndex: 'State',xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.ScheduleStateTemplate',
{
states: ['Defined', 'In-Progress', 'Completed'],
field: {
name: 'State'
}
})
},
{
text: 'Estimate',dataIndex: 'Estimate',
summaryType: 'sum',
},
{
text: 'WorkProduct',dataIndex: 'WorkProduct',
renderer: function(val, meta, record) {
return '' + record.get('WorkProduct').FormattedID + '';
}
},
]
});
}
Update: If you want to filter the task store include a filter here:
tasks.store = story.getCollection('Tasks',{fetch:['Name','FormattedID','Estimate','State','Blocked','WorkProduct'],filters:{property: 'State',operator: '<',value: 'Completed'}});
A treegrid example: Rally.ui.grid.TreeGrid you referred is still a work in progress. I have not seen a working example of a story hierarchy using a treegrid but it does not mean it's impossible.
When I tested a story hierarchy, child stories did not appear under epic stories, however a story/task hierarchy worked. The filtering worked too. Here is an example:
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch:function(){
Ext.create('Rally.data.wsapi.TreeStoreBuilder').build({
models: ['userstory'],
autoLoad: true,
filters:[
{
property: 'Name',
operator: 'contains',
value: 'story'
}
],
enableHierarchy: true
}).then({
success: function(store) {
var grid = Ext.create('Ext.Container', {
items: [{
xtype: 'rallytreegrid',
columnCfgs: [
'Name',
'Owner'
],
store: store
}]
});
that.add(grid);
}
});
}
The screenshot below shows that tasks are nested under a child story as expected,but the child story is not nested under parent. The grid is filtered by Name as expected:

Related

How to add custom data in a story board?

Question 1: Is it possible to add custom data (Ex: testcase count : 5) to each card in a story board? If so, how? I couldn't find an example or specific information in documentation.
Question 2: Is it possible to get testcase count (including child story testcases)for a highlevel story in one query?
Please let me know. Here's my code
Ext.define('Rally.Story.View', {
extend: 'Rally.app.App',
launch: function() {
this.add({
xtype: 'rallyfieldvaluecombobox',
fieldLabel: 'Filter by Target Release:',
model: 'UserStory',
field: 'c_TargetRelease',
value: '15.0',
listeners: {
select: this._onSelect,
ready: this._onLoad,
scope: this
}
});
},
_onLoad: function() {
this.add({
xtype: 'rallycardboard',
types: ['User Story'],
attribute: 'ScheduleState',
readOnly: true,
fetch: ['Name', 'TestCases', 'c_StoryType', 'c_TargetRelease', 'PlanEstimate', 'Priority', 'TaskEstimateTotal', 'TaskRemainingTotal'],
context: this.getContext(),
cardConfig: {
editable: false,
showIconsAndHighlightBorder: false,
fields: ['Name', 'c_StoryType', 'c_TargetRelease', 'PlanEstimate', 'c_PriorityBin', 'Parent', 'TestCases', 'TaskEstimateTotal', 'TaskRemainingTotal']
},
storeConfig: {
filters: [
{
property: 'c_StoryType',
value: 'SAGA Feature'
},
{
property: 'c_TargetRelease',
operator: '=',
value: this.down('rallyfieldvaluecombobox').getValue()
}
]
}
});
},
_onSelect: function() {
var board = this.down('rallycardboard');
board.refresh({
storeConfig: {
filters: [
{
property: 'c_StoryType',
value: 'SAGA Feature'
},
{
property: 'c_TargetRelease',
operator: '=',
value: this.down('rallyfieldvaluecombobox').getValue()
}
]
}
});
},
});
Here's a sample card I made that contains the test case count:
You can add a field by simply including an object with some rendering information in it instead of just a simple string in the fields array in the cardConfig:
cardConfig: {
fields: [
'Name', //simple string field to show
{
name: 'TCCount', //field name
hasValue: function() {return true;}, //always show this field
renderTpl: Ext.create('Rally.ui.renderer.template.LabeledFieldTemplate', {
fieldLabel: 'Test Case Count', //the field label
valueTemplate: Ext.create('Ext.XTemplate',
['{[this.getTestCaseCount(values)]}',
{getTestCaseCount: function(data) { return data.Summary.TestCases.Count;}}])
})
},
//additional string fields
'PlanEstimate', 'Parent', 'TestCases', 'TaskEstimateTotal', 'TaskRemainingTotal']
}
This ended up being less straightforward than I thought it might be, but at least it is doable. The key part is using the LabeledFieldTemplate, specifying a field label and a value template to actually render the content.
You'll also notice the little beaker status icon in the footer which is automatically rendered because TestCases was included in the fields list.
As for your second question there is no roll up field on story for the total number of test cases included on child stories.

How to display ScheduleState and Blocked columns in custom Grid the way Rally's grid does

I'm trying to create a custom Rally app that displays data in a grid view. In another question Rally SDK App Using Grid with collapsible tree of children stories, nickm posted some sample code
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
var today = new Date().toISOString();
Ext.create('Rally.data.wsapi.Store', {
model: 'UserStory',
fetch: ['ObjectID', 'FormattedID', 'Name', 'ScheduleState', 'Feature'],
autoLoad: true,
filters: [
{
property: 'Iteration.StartDate',
operator: '<=',
value: today
},
{
property: 'Iteration.EndDate',
operator: '>=',
value: today
},
{
property: 'Feature',
operator: '!=',
value: null
}
],
listeners: {
load: this._onDataLoaded,
scope: this
}
});
},
_onDataLoaded: function(store, records){
var that = this;
var promises = [];
_.each(records, function(story) {
promises.push(that._getFeature(story, that));
});
Deft.Promise.all(promises).then({
success: function(results) {
that._stories = results;
that._makeGrid();
}
});
},
_getFeature: function(story, scope) {
var deferred = Ext.create('Deft.Deferred');
var that = scope;
var featureOid = story.get('Feature').ObjectID;
Rally.data.ModelFactory.getModel({
type: 'PortfolioItem/Feature',
scope: this,
success: function(model, operation) {
fetch: ['State'],
model.load(featureOid, {
scope: this,
success: function(record, operation) {
var featureState = record.get('State')._refObjectName;
var storyRef = story.get('_ref');
var storyOid = story.get('ObjectID');
var storyFid = story.get('FormattedID');
var storyName = story.get('Name');
var storyState = story.get('ScheduleState');
var feature = story.get('Feature');
result = {
"_ref" : storyRef,
"ObjectID" : storyOid,
"FormattedID" : storyFid,
"Name" : storyName,
"ScheduleState" : storyState,
"Feature" : feature,
"FeatureState" : featureState,
"FeatureID" : featureOid
};
deferred.resolve(result);
}
});
}
});
return deferred;
},
_makeGrid: function() {
var that = this;
if (that._grid) {
that._grid.destroy();
}
var gridStore = Ext.create('Rally.data.custom.Store', {
data: that._stories,
groupField: 'FeatureID',
pageSize: 1000,
});
that._grid = Ext.create('Rally.ui.grid.Grid', {
itemId: 'storyGrid',
store: gridStore,
features: [{ftype:'grouping'}],
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name',
},
{
text: 'ScheduleState', dataIndex: 'ScheduleState',
},
{
text: 'Feature', dataIndex: 'Feature',
renderer: function(val, meta, record) {
return '' + record.get('Feature').FormattedID + '';
}
},
{
text: 'Feature State', dataIndex: 'FeatureState',
}
]
});
that.add(that._grid);
that._grid.reconfigure(gridStore);
}
});
I'd like to display the ScheduleState and Blocked columns the same way that the Rally Grid shows them (as graphic representations). I've tried to figure out how to use templatecolumn xtype by using the following in my columnCfgs block:
{ text: 'State', dataIndex: 'ScheduleState', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.ScheduleStateTemplate') }
This fails and causes a JS error in the sdk-debug.js:
Uncaught TypeError: Cannot read property 'getAllowedValueStore' of
undefined sdk-debug.js:190539 Ext.define.loadStates
I get different errors with the Blocked column but I haven't been able to figure out how to get it to display as the red blocked icon.
With some tweaking I expect this to work in the next release of AppSDK2, but right now the rendering of the ScheduleState and Blocked will only work with wsapi data store. Using
tpl: Ext.create('Rally.ui.renderer.template.ScheduleStateTemplate')
is not sufficient. Custom store has no access to the valid states.
UPDATE
Rendering of ScheduleState and Blocked works in the current x version of AppSDK2 as of the date of this update, 5/22
<script type="text/javascript" src="/apps/x/sdk.js"></script>
This fix will eventually make its way to the next official release of AppSDK2 but for now you may use it with x version of AppSDK2.
Warning: x version of AppSDK is never stable - changes are made to it constantly.
See full code example here.
In x version of AppSDK you may do this:
{
text: 'ScheduleState', dataIndex: 'ScheduleState', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.ScheduleStateTemplate',
{
states: ['Defined', 'In-Progress', 'Completed', 'Accepted'],
field: {name: 'ScheduleState' }
})
},
{
text: 'Blocked', dataIndex: 'Blocked', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.BlockedTemplate')
}
Also, for now, set this to false in the grid config:
enableBlockedReasonPopover: false

Query Rally Lookback API with owner name in output

I want a list of all the people who have revised the given user story in addition to other data.
I'm querying the Rally Lookback REST API with the following JSON data:
{
"find" : { "FormattedID": "$STORY" },
"fields" : ["ObjectID", "_ValidFrom", "_ValidTo", "Blocked", "c_KanbanState", "Owner"],
"compress" : true
}
With this query, I get the Owner's OID, like so:
{
"_ValidFrom": "2014-05-09T15:18:29.912Z",
"_ValidTo": "9999-01-01T00:00:00.000Z",
"ObjectID": 18326652440,
"Blocked": false,
"Owner": 13786838413,
"c_KanbanState": "Accepted"
}
Is there a way to hydrate that Owner field? I'd like to see "John Smith", but I'd settle for "jsmith#example.com".
If I have to use the WSAPI for this, is there a way to query for a group of Owner OIDs at once -- if so, a sample would be helpful -- or will I need to loop through the collection of values and query each Owner individually?
As Trever said, in Lookback API user fields cannot be hydrated.
As far as an example that hydrates user fields using wsapi, the code below uses shapshotstore to get snapshots where '_PreviousValues.Blocked' : {$exists: true}, and then uses Rally.data.ModelFactory to get the DisplayName of owner in each snapshot.
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
scopeType: 'iteration',
comboboxConfig: {
labelWidth: 100,
width: 300
},
launch: function() {
var that = this;
var iterationComboBox = Ext.create('Rally.ui.combobox.IterationComboBox',{
listeners:{
ready: function(combobox){
var iterationOid = combobox.getRecord().get('ObjectID');
that._loadStories(iterationOid);
},
select: function(combobox){
var iterationOid = combobox.getRecord().get('ObjectID');
this._loadStories(iterationOid);
},
scope: this
}
});
this.add(iterationComboBox);
},
_loadStories:function(iterationOid){
var that = this;
var snapshotStore = Ext.create('Rally.data.lookback.SnapshotStore', {
autoLoad:true,
find: {
'_TypeHierarchy': 'HierarchicalRequirement',
'_ProjectHierarchy': 12352608219,
'_PreviousValues.Blocked' : {$exists: true},
'Iteration': iterationOid
},
fetch: ['Name','FormattedID','ScheduleState','Blocked','_ValidFrom','_ValidTo', 'BlockedReason','Owner'],
order: 'OpenedDate DESC',
hydrate: ['Blocked','ScheduleState'],
compress: true,
listeners: {
load: function(store,records,success){
console.log("loaded %i records", records.length);
that._onStoriesLoaded(snapshotStore, records);
},
scope:this
}
});
},
_onStoriesLoaded:function(store, records){
var that = this;
var promises = [];
_.each(records, function(story) {
promises.push(that._hydrateOwner(story, that));
});
Deft.Promise.all(promises).then({
success: function(results) {
that._stories = results;
console.log('that._stories', that._stories);
that._makeGrid();
}
});
},
_hydrateOwner:function(story, scope){
var deferred = Ext.create('Deft.Deferred');
var that = scope;
var ownerDisplayName = null;
var userOid = story.get('Owner');
var storyBlocked = story.get('Blocked');
Rally.data.ModelFactory.getModel({
type: 'User',
scope: this,
success: function(model, operation) {
fetch: ['UserName', 'DisplayName'],
model.load(userOid, {
scope: this,
success: function(record, operation) {
owner = record.get('DisplayName');
var fid = story.get('FormattedID');
var state = story.get('ScheduleState');
var name = story.get('Name');
var blocked = story.get('Blocked');
result = {
"fid" : fid,
"name" : name,
"state" : state,
"blocked" : blocked,
"owner" : owner
};
deferred.resolve(result);
}
});
}
});
return deferred;
},
_makeGrid: function() {
if (this.down('#grid')) {
this.down('#grid').destroy();
}
var gridStore = Ext.create('Rally.data.custom.Store', {
data: this._stories
});
var _grid = Ext.create('Rally.ui.grid.Grid', {
itemId: 'grid',
store: gridStore,
columnCfgs: [
{
text: 'Name', dataIndex: 'name'
},
{
text: 'FormattedID', dataIndex: 'fid'
},
{
text: 'ScheduleState', dataIndex: 'state'
},
{
text: 'Blocked', dataIndex: 'blocked'
},
{
text: 'Owner', dataIndex: 'owner'
}
]
});
this.add(_grid);
this._grid.reconfigure(gridStore);
}
});
Unfortunately, per the documentation -
It is not possible to hydrate some field types (e.g. User).
See the hydration section of the documentation
And to add to what Nick and Trever have said, if you want to know who has revised a given story, the field you're looking for is "_User". Owner is the owner, _User is who created the revision. Nick's example code can be tweaked to hydrate the _User, since it's just an OID like Owner.
A caveat: if someone changed Only a big text field (like the Description), that doesn't create a snapshot so won't be returned.

Linking SSO users to item's detail page

I am displaying snapshot store records in a rallygrid component and would like to make it so the ID field is clickable and brings up the detail page for that work item. Since snapshot records include an "_UnformattedID" rather than "FormattedID", I tried to accomplish this using a column renderer, which adds the text as a link:
renderer: function(val, meta, record) {
return 'US' + record.get('_UnformattedID') + '';
}
This works perfectly for me, as a non-SSO user, but users in our workspace who are using SSO have reported that the link simply brings them to their default start page. Not the detail page they were expecting.
Is there a better way I could be accomplishing this that would allow all users the functionality?
SSO implementation is different across organization, but this trick worked for me.
I detect the host:
this._host = window.location.hostname;
and then I use host when building the return value of the renderer since the resulting URL in SSO and Non-SSO scenarios in my environment only differ in the host portion.
{
text: 'Formatted ID', dataIndex: 'UnformattedID',
renderer: function(val, meta, record) {
return 'US' + record.get('UnformattedID') + '';
}
}
.
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
this._host = window.location.hostname;
console.log('host', this._host);
var iterationComboBox = Ext.create('Rally.ui.combobox.IterationComboBox',{
listeners:{
ready: function(combobox){
this._iterationOid = combobox.getRecord().get('ObjectID');
this._loadStories(this._iterationOid);
},
select: function(combobox){
this._iterationOid = combobox.getRecord().get('ObjectID');
this._loadStories(this._iterationOid);
},
scope: this
}
});
this.add(iterationComboBox);
},
_loadStories:function(iterationOid){
console.log('loading stories for ', iterationOid);
var myStore = Ext.create('Rally.data.lookback.SnapshotStore', {
autoLoad:true,
fetch : ['Name','_UnformattedID','ScheduleState','_TypeHierarchy'],
filters : [{
property : '__At',
value : 'current'
},
{
property : '_TypeHierarchy',
value : 'HierarchicalRequirement'
},
{
property : 'Iteration',
value : iterationOid
}
],
hydrate: ['_TypeHierarchy'],
listeners: {
load: function(store,records,success){
console.log("loaded %i records", records.length);
this._onDataLoaded(myStore, records);
},
scope:this
}
});
},
_onDataLoaded: function(store,data){
console.log('count',store.getCount());
var that = this;
var records = [];
Ext.Array.each(data, function(record) {
records.push({
Name: record.get('Name'),
ObjectID: record.get('ObjectID'),
UnformattedID: record.get('_UnformattedID')
});
});
var myStore = Ext.create('Rally.data.custom.Store', {
data: records
});
if (!this.down('#grid')) {
this.add({
xtype: 'rallygrid',
id: 'grid',
store: myStore,
columnCfgs: [
{
text: 'Name', dataIndex: 'Name', flex: 1
},
{
text: 'Formatted ID', dataIndex: 'UnformattedID',
renderer: function(val, meta, record) {
return 'US' + record.get('UnformattedID') + '';
}
}
]
});
}
else{
console.log(store);
this.down('#grid').reconfigure(myStore);
}
}
});

Custom Rally Grid column together with Rally data columns

I am trying to create an app that will display all test sets in the current project and their states in terms of pass/fail totals.
Issues that I face (BTW just started learning ExtJS and Rally SDK yesterday):
- I need to understand how can I use the currently selected project as the one sued in the grid as a filter
- how should a test set pass/fail totals be queried and then displayed in a column in the grid - example : Test set 123 | 45/70
This is an example of an app that uses a project picker and builds a grid of Test Sets by Project. Also see a code example from this post. There is no filed in Web Services API where pass/fail totals are calculated. You will have to iterate over results and calculate totals in your code. I would encourage limiting the number of test case results by some criteria, e.g. CreationDate. In a scenario when test case results are automated the sheer volume of the data can be problematic.
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
var c = Ext.create('Ext.Container', {
items: [
{
xtype: 'rallyprojectpicker',
fieldLabel: 'select project',
listeners:{
change: function(combobox){
if ( this.down('#g')) {
console.log('grid exists');
Ext.getCmp('g').destroy();
console.log('grid deleted');
}
this.onProjectSelected(combobox.getSelectedRecord());
},
scope: this
}
},
],
});
this.add(c);
},
onProjectSelected:function(record){
var project = record.data['_ref'];
console.log('project', project);
var testSetStore = Ext.create('Rally.data.WsapiDataStore', {
model: 'TestSet',
fetch: ['FormattedID','Name', 'Project', 'TestCaseStatus'],
pageSize: 100,
autoLoad: true,
filters: [
{
property: 'Project',
value: project
}
],
listeners: {
load: this.onTestSetsLoaded,
scope: this
}
});
},
onTestSetsLoaded:function(store, data){
var testSets = [];
Ext.Array.each(data, function(testset) {
var ts = {
FormattedID: testset.get('FormattedID'),
_ref: testset.get("_ref"),
Name: testset.get('Name'),
TestCaseStatus: testset.get('TestCaseStatus')
};
testSets.push(ts);
});
this.updateGrid(testSets);
},
updateGrid: function(testSets){
var store = Ext.create('Rally.data.custom.Store', {
data: testSets,
pageSize: 100
});
if (!this.down('#g')) {
this.createGrid(store);
}
else{
this.down('#g').reconfigure(store);
}
},
createGrid: function(store){
console.log("load grid", store);
var g = Ext.create('Rally.ui.grid.Grid', {
id: 'g',
store: store,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name'
},
{
text: 'TestCaseStatus', dataIndex: 'TestCaseStatus'
}
]
});
this.add(g);
},
});
The full html source of this example is available from this repo