@@ -3684,4 +3684,347 @@ describe('MCP Dashboard Tools - clickstack_save_dashboard', () => {
36843684 expect ( getFirstText ( result ) ) . toContain ( 'only valid for gauge metrics' ) ;
36853685 } ) ;
36863686 } ) ;
3687+
3688+ describe ( 'savedFilterValues' , ( ) => {
3689+ const tile = ( sourceId : string ) => ( {
3690+ name : 'Volume' ,
3691+ config : {
3692+ displayType : 'line' as const ,
3693+ sourceId,
3694+ select : [ { aggFn : 'count' as const , where : '' } ] ,
3695+ } ,
3696+ } ) ;
3697+
3698+ it ( 'persists savedFilterValues on create and echoes them back' , async ( ) => {
3699+ const sourceId = ctx . traceSource . _id . toString ( ) ;
3700+
3701+ const result = await callTool ( ctx . client ! , 'clickstack_save_dashboard' , {
3702+ name : 'Defaults on create' ,
3703+ tiles : [ tile ( sourceId ) ] ,
3704+ filters : [
3705+ {
3706+ type : 'QUERY_EXPRESSION' ,
3707+ name : 'Service' ,
3708+ expression : 'ServiceName' ,
3709+ sourceId,
3710+ whereLanguage : 'sql' ,
3711+ isVariableEnabled : true ,
3712+ variableName : 'service' ,
3713+ } ,
3714+ ] ,
3715+ savedFilterValues : [
3716+ { type : 'sql' , condition : "ServiceName IN ('api')" } ,
3717+ { type : 'variable' , name : 'service' , values : [ 'api' , 'web' ] } ,
3718+ ] ,
3719+ } ) ;
3720+
3721+ expect ( result . isError ) . toBeFalsy ( ) ;
3722+ const created = JSON . parse ( getFirstText ( result ) ) ;
3723+ expect ( created . savedFilterValues ) . toEqual ( [
3724+ { type : 'sql' , condition : "ServiceName IN ('api')" } ,
3725+ { type : 'variable' , name : 'service' , values : [ 'api' , 'web' ] } ,
3726+ ] ) ;
3727+
3728+ const stored = await Dashboard . findById ( created . id ) ;
3729+ expect ( stored ?. savedFilterValues ) . toEqual ( [
3730+ { type : 'sql' , condition : "ServiceName IN ('api')" } ,
3731+ { type : 'variable' , name : 'service' , values : [ 'api' , 'web' ] } ,
3732+ ] ) ;
3733+ } ) ;
3734+
3735+ it ( 'defaults an omitted type to sql on a condition entry' , async ( ) => {
3736+ const sourceId = ctx . traceSource . _id . toString ( ) ;
3737+
3738+ const result = await callTool ( ctx . client ! , 'clickstack_save_dashboard' , {
3739+ name : 'Implicit sql type' ,
3740+ tiles : [ tile ( sourceId ) ] ,
3741+ savedFilterValues : [ { condition : "ServiceName IN ('api')" } ] ,
3742+ } ) ;
3743+
3744+ expect ( result . isError ) . toBeFalsy ( ) ;
3745+ const created = JSON . parse ( getFirstText ( result ) ) ;
3746+ expect ( created . savedFilterValues ) . toEqual ( [
3747+ { type : 'sql' , condition : "ServiceName IN ('api')" } ,
3748+ ] ) ;
3749+ } ) ;
3750+
3751+ it ( 'returns savedFilterValues from clickstack_get_dashboard' , async ( ) => {
3752+ const sourceId = ctx . traceSource . _id . toString ( ) ;
3753+
3754+ const createResult = await callTool (
3755+ ctx . client ! ,
3756+ 'clickstack_save_dashboard' ,
3757+ {
3758+ name : 'Defaults readable on get' ,
3759+ tiles : [ tile ( sourceId ) ] ,
3760+ savedFilterValues : [ { type : 'sql' , condition : 'StatusCode = 200' } ] ,
3761+ } ,
3762+ ) ;
3763+ const created = JSON . parse ( getFirstText ( createResult ) ) ;
3764+
3765+ const getResult = await callTool (
3766+ ctx . client ! ,
3767+ 'clickstack_get_dashboard' ,
3768+ { id : created . id } ,
3769+ ) ;
3770+ const fetched = JSON . parse ( getFirstText ( getResult ) ) ;
3771+ expect ( fetched . savedFilterValues ) . toEqual ( [
3772+ { type : 'sql' , condition : 'StatusCode = 200' } ,
3773+ ] ) ;
3774+ } ) ;
3775+
3776+ it ( 'leaves saved defaults untouched when the update omits the field' , async ( ) => {
3777+ const sourceId = ctx . traceSource . _id . toString ( ) ;
3778+
3779+ const createResult = await callTool (
3780+ ctx . client ! ,
3781+ 'clickstack_save_dashboard' ,
3782+ {
3783+ name : 'Keep defaults on update' ,
3784+ tiles : [ tile ( sourceId ) ] ,
3785+ savedFilterValues : [ { type : 'sql' , condition : 'StatusCode = 200' } ] ,
3786+ } ,
3787+ ) ;
3788+ const created = JSON . parse ( getFirstText ( createResult ) ) ;
3789+
3790+ const updateResult = await callTool (
3791+ ctx . client ! ,
3792+ 'clickstack_save_dashboard' ,
3793+ {
3794+ id : created . id ,
3795+ name : 'Keep defaults on update (renamed)' ,
3796+ tiles : [ tile ( sourceId ) ] ,
3797+ } ,
3798+ ) ;
3799+ expect ( updateResult . isError ) . toBeFalsy ( ) ;
3800+ const updated = JSON . parse ( getFirstText ( updateResult ) ) ;
3801+ expect ( updated . savedFilterValues ) . toEqual ( [
3802+ { type : 'sql' , condition : 'StatusCode = 200' } ,
3803+ ] ) ;
3804+ } ) ;
3805+
3806+ it ( 'clears saved defaults when the update passes an empty array' , async ( ) => {
3807+ const sourceId = ctx . traceSource . _id . toString ( ) ;
3808+
3809+ const createResult = await callTool (
3810+ ctx . client ! ,
3811+ 'clickstack_save_dashboard' ,
3812+ {
3813+ name : 'Clear defaults on update' ,
3814+ tiles : [ tile ( sourceId ) ] ,
3815+ savedFilterValues : [ { type : 'sql' , condition : 'StatusCode = 200' } ] ,
3816+ } ,
3817+ ) ;
3818+ const created = JSON . parse ( getFirstText ( createResult ) ) ;
3819+
3820+ const updateResult = await callTool (
3821+ ctx . client ! ,
3822+ 'clickstack_save_dashboard' ,
3823+ {
3824+ id : created . id ,
3825+ name : 'Clear defaults on update' ,
3826+ tiles : [ tile ( sourceId ) ] ,
3827+ savedFilterValues : [ ] ,
3828+ } ,
3829+ ) ;
3830+ expect ( updateResult . isError ) . toBeFalsy ( ) ;
3831+ const updated = JSON . parse ( getFirstText ( updateResult ) ) ;
3832+ expect ( updated . savedFilterValues ) . toEqual ( [ ] ) ;
3833+
3834+ const stored = await Dashboard . findById ( created . id ) ;
3835+ expect ( stored ?. savedFilterValues ) . toEqual ( [ ] ) ;
3836+ } ) ;
3837+
3838+ it ( 'sets savedFilterValues via clickstack_patch_dashboard' , async ( ) => {
3839+ const sourceId = ctx . traceSource . _id . toString ( ) ;
3840+
3841+ const createResult = await callTool (
3842+ ctx . client ! ,
3843+ 'clickstack_save_dashboard' ,
3844+ {
3845+ name : 'Patch defaults' ,
3846+ tiles : [ tile ( sourceId ) ] ,
3847+ } ,
3848+ ) ;
3849+ const created = JSON . parse ( getFirstText ( createResult ) ) ;
3850+
3851+ const patchResult = await callTool (
3852+ ctx . client ! ,
3853+ 'clickstack_patch_dashboard' ,
3854+ {
3855+ dashboardId : created . id ,
3856+ savedFilterValues : [
3857+ { type : 'sql' , condition : "ServiceName IN ('api')" } ,
3858+ ] ,
3859+ } ,
3860+ ) ;
3861+ expect ( patchResult . isError ) . toBeFalsy ( ) ;
3862+ const patched = JSON . parse ( getFirstText ( patchResult ) ) ;
3863+ expect ( patched . savedFilterValues ) . toEqual ( [
3864+ { type : 'sql' , condition : "ServiceName IN ('api')" } ,
3865+ ] ) ;
3866+
3867+ const stored = await Dashboard . findById ( created . id ) ;
3868+ expect ( stored ?. savedFilterValues ) . toEqual ( [
3869+ { type : 'sql' , condition : "ServiceName IN ('api')" } ,
3870+ ] ) ;
3871+
3872+ // Tiles are preserved by the targeted patch.
3873+ expect ( stored ?. tiles ) . toHaveLength ( 1 ) ;
3874+ } ) ;
3875+
3876+ it ( 'patches a tile and savedFilterValues in the same call' , async ( ) => {
3877+ const sourceId = ctx . traceSource . _id . toString ( ) ;
3878+
3879+ const createResult = await callTool (
3880+ ctx . client ! ,
3881+ 'clickstack_save_dashboard' ,
3882+ { name : 'Combined patch' , tiles : [ tile ( sourceId ) ] } ,
3883+ ) ;
3884+ const created = JSON . parse ( getFirstText ( createResult ) ) ;
3885+ const tileId = created . tiles [ 0 ] . id ;
3886+
3887+ const patchResult = await callTool (
3888+ ctx . client ! ,
3889+ 'clickstack_patch_dashboard' ,
3890+ {
3891+ dashboardId : created . id ,
3892+ savedFilterValues : [
3893+ { type : 'sql' , condition : "ServiceName IN ('api')" } ,
3894+ ] ,
3895+ tileId,
3896+ tile : {
3897+ config : {
3898+ displayType : 'table' ,
3899+ sourceId,
3900+ select : [ { aggFn : 'count' , where : '' } ] ,
3901+ } ,
3902+ } ,
3903+ } ,
3904+ ) ;
3905+ expect ( patchResult . isError ) . toBeFalsy ( ) ;
3906+ const patched = JSON . parse ( getFirstText ( patchResult ) ) ;
3907+ expect ( patched . savedFilterValues ) . toEqual ( [
3908+ { type : 'sql' , condition : "ServiceName IN ('api')" } ,
3909+ ] ) ;
3910+ expect ( patched . patchedTile . config . displayType ) . toBe ( 'table' ) ;
3911+
3912+ // Both the tile and the saved values changed in the database.
3913+ const stored = await Dashboard . findById ( created . id ) ;
3914+ expect ( stored ?. tiles ) . toHaveLength ( 1 ) ;
3915+ expect ( stored ?. tiles [ 0 ] . id ) . toBe ( tileId ) ;
3916+ expect ( stored ?. tiles [ 0 ] . config . displayType ) . toBe ( 'table' ) ;
3917+ expect ( stored ?. savedFilterValues ) . toEqual ( [
3918+ { type : 'sql' , condition : "ServiceName IN ('api')" } ,
3919+ ] ) ;
3920+ } ) ;
3921+
3922+ it ( 'warns when a variable saved value names no declared filter' , async ( ) => {
3923+ const sourceId = ctx . traceSource . _id . toString ( ) ;
3924+
3925+ const result = await callTool ( ctx . client ! , 'clickstack_save_dashboard' , {
3926+ name : 'Variable default typo' ,
3927+ tiles : [ tile ( sourceId ) ] ,
3928+ filters : [
3929+ {
3930+ type : 'QUERY_EXPRESSION' ,
3931+ name : 'Service' ,
3932+ expression : 'ServiceName' ,
3933+ sourceId,
3934+ whereLanguage : 'sql' ,
3935+ isVariableEnabled : true ,
3936+ variableName : 'service' ,
3937+ } ,
3938+ ] ,
3939+ savedFilterValues : [
3940+ { type : 'variable' , name : 'srvice' , values : [ 'api' ] } ,
3941+ ] ,
3942+ } ) ;
3943+
3944+ expect ( result . isError ) . toBeFalsy ( ) ;
3945+ const created = JSON . parse ( getFirstText ( result ) ) ;
3946+ expect ( created . warnings ) . toEqual (
3947+ expect . arrayContaining ( [ expect . stringContaining ( '$srvice' ) ] ) ,
3948+ ) ;
3949+ } ) ;
3950+
3951+ it ( 'does not warn when a variable saved value matches a declared filter' , async ( ) => {
3952+ const sourceId = ctx . traceSource . _id . toString ( ) ;
3953+
3954+ const result = await callTool ( ctx . client ! , 'clickstack_save_dashboard' , {
3955+ name : 'Variable default match' ,
3956+ tiles : [ tile ( sourceId ) ] ,
3957+ filters : [
3958+ {
3959+ type : 'QUERY_EXPRESSION' ,
3960+ name : 'Service' ,
3961+ expression : 'ServiceName' ,
3962+ sourceId,
3963+ whereLanguage : 'sql' ,
3964+ isVariableEnabled : true ,
3965+ variableName : 'service' ,
3966+ } ,
3967+ ] ,
3968+ savedFilterValues : [
3969+ { type : 'variable' , name : 'service' , values : [ 'api' ] } ,
3970+ ] ,
3971+ } ) ;
3972+
3973+ expect ( result . isError ) . toBeFalsy ( ) ;
3974+ const created = JSON . parse ( getFirstText ( result ) ) ;
3975+ const savedValueWarnings = ( created . warnings ?? [ ] ) . filter ( ( w : string ) =>
3976+ w . includes ( 'savedFilterValues' ) ,
3977+ ) ;
3978+ expect ( savedValueWarnings ) . toEqual ( [ ] ) ;
3979+ } ) ;
3980+
3981+ it ( 'warns on a patch whose variable saved value names no declared filter' , async ( ) => {
3982+ const sourceId = ctx . traceSource . _id . toString ( ) ;
3983+
3984+ const createResult = await callTool (
3985+ ctx . client ! ,
3986+ 'clickstack_save_dashboard' ,
3987+ { name : 'Patch variable typo' , tiles : [ tile ( sourceId ) ] } ,
3988+ ) ;
3989+ const created = JSON . parse ( getFirstText ( createResult ) ) ;
3990+
3991+ const patchResult = await callTool (
3992+ ctx . client ! ,
3993+ 'clickstack_patch_dashboard' ,
3994+ {
3995+ dashboardId : created . id ,
3996+ savedFilterValues : [
3997+ { type : 'variable' , name : 'ghost' , values : [ 'api' ] } ,
3998+ ] ,
3999+ } ,
4000+ ) ;
4001+ expect ( patchResult . isError ) . toBeFalsy ( ) ;
4002+ const patched = JSON . parse ( getFirstText ( patchResult ) ) ;
4003+ expect ( patched . warnings ) . toEqual (
4004+ expect . arrayContaining ( [ expect . stringContaining ( '$ghost' ) ] ) ,
4005+ ) ;
4006+ } ) ;
4007+
4008+ it ( 'rejects a patch with no updatable fields' , async ( ) => {
4009+ const sourceId = ctx . traceSource . _id . toString ( ) ;
4010+
4011+ const createResult = await callTool (
4012+ ctx . client ! ,
4013+ 'clickstack_save_dashboard' ,
4014+ {
4015+ name : 'Empty patch' ,
4016+ tiles : [ tile ( sourceId ) ] ,
4017+ } ,
4018+ ) ;
4019+ const created = JSON . parse ( getFirstText ( createResult ) ) ;
4020+
4021+ const patchResult = await callTool (
4022+ ctx . client ! ,
4023+ 'clickstack_patch_dashboard' ,
4024+ { dashboardId : created . id } ,
4025+ ) ;
4026+ expect ( patchResult . isError ) . toBe ( true ) ;
4027+ expect ( getFirstText ( patchResult ) ) . toContain ( 'savedFilterValues' ) ;
4028+ } ) ;
4029+ } ) ;
36874030} ) ;
0 commit comments