3
3
4
4
package software.aws.toolkits.jetbrains.core.experiments
5
5
6
+ import com.intellij.openapi.actionSystem.AnActionEvent
7
+ import com.intellij.openapi.application.ApplicationManager
6
8
import com.intellij.openapi.components.BaseState
7
9
import com.intellij.openapi.components.PersistentStateComponent
8
10
import com.intellij.openapi.components.State
9
11
import com.intellij.openapi.components.Storage
10
12
import com.intellij.openapi.components.service
11
13
import com.intellij.openapi.extensions.ExtensionPointName
14
+ import com.intellij.openapi.project.DumbAwareAction
15
+ import com.intellij.util.messages.Topic
12
16
import com.intellij.util.xmlb.annotations.Property
17
+ import software.aws.toolkits.core.utils.replace
13
18
import software.aws.toolkits.jetbrains.AwsToolkit
19
+ import software.aws.toolkits.jetbrains.utils.createNotificationExpiringAction
20
+ import software.aws.toolkits.jetbrains.utils.notifyInfo
21
+ import software.aws.toolkits.resources.message
14
22
import software.aws.toolkits.telemetry.AwsTelemetry
15
23
import software.aws.toolkits.telemetry.ExperimentState.Activated
16
24
import software.aws.toolkits.telemetry.ExperimentState.Deactivated
25
+ import java.time.Duration
26
+ import java.time.Instant
17
27
18
28
/* *
19
29
* Used to control the state of an experimental feature.
@@ -22,6 +32,7 @@ import software.aws.toolkits.telemetry.ExperimentState.Deactivated
22
32
*
23
33
* @param hidden determines whether this experiment should surface in the settings/menus; hidden experiments can only be enabled by system property or manually modifying config in aws.xml
24
34
* @param default determines the default state of an experiment
35
+ * @param suggestionSnooze how long to wait between prompting a suggestion to enable the experiment (when using the experiment suggestion system ([ToolkitExperiment.suggest]))
25
36
*
26
37
* `ToolkitExperiment` implementations should be an `object` for example:
27
38
*
@@ -42,7 +53,8 @@ abstract class ToolkitExperiment(
42
53
internal val title : () -> String ,
43
54
internal val description : () -> String ,
44
55
internal val hidden : Boolean = false ,
45
- internal val default : Boolean = false
56
+ internal val default : Boolean = false ,
57
+ internal val suggestionSnooze : Duration = Duration .ofDays(7)
46
58
) {
47
59
override fun equals (other : Any? ) = (other as ? ToolkitExperiment )?.id?.equals(id) == true
48
60
override fun hashCode () = id.hashCode()
@@ -51,13 +63,48 @@ abstract class ToolkitExperiment(
51
63
fun ToolkitExperiment.isEnabled (): Boolean = ToolkitExperimentManager .getInstance().isEnabled(this )
52
64
internal fun ToolkitExperiment.setState (enabled : Boolean ) = ToolkitExperimentManager .getInstance().setState(this , enabled)
53
65
66
+ /* *
67
+ * Surface a notification suggesting that the given experiment be enabled.
68
+ */
69
+ fun ToolkitExperiment.suggest () {
70
+ if (ToolkitExperimentManager .getInstance().shouldPrompt(this )) {
71
+ notifyInfo(
72
+ title = message(" aws.toolkit.experimental.suggestion.title" ),
73
+ content = message(" aws.toolkit.experimental.suggestion.description" , title(), description()),
74
+ notificationActions = listOf (
75
+ createNotificationExpiringAction(EnableExperiment (this )),
76
+ createNotificationExpiringAction(NeverShowAgain (this ))
77
+ ),
78
+ stripHtml = false
79
+ )
80
+ }
81
+ }
82
+
83
+ private class EnableExperiment (private val experiment : ToolkitExperiment ) :
84
+ DumbAwareAction (message(" aws.toolkit.experimental.enable" )) {
85
+ override fun actionPerformed (e : AnActionEvent ) {
86
+ experiment.setState(true )
87
+ }
88
+ }
89
+
90
+ private class NeverShowAgain (private val experiment : ToolkitExperiment ) : DumbAwareAction(message("settings.never_show_again")) {
91
+ override fun actionPerformed (e : AnActionEvent ) {
92
+ ToolkitExperimentManager .getInstance().neverPrompt(experiment)
93
+ }
94
+ }
95
+
54
96
@State(name = " experiments" , storages = [Storage (" aws.xml" )])
55
97
internal class ToolkitExperimentManager : PersistentStateComponent <ExperimentState > {
56
- private val enabledState = mutableMapOf<String , Boolean >()
98
+ private val state = ExperimentState ()
99
+ private val enabledState get() = state.value
100
+
57
101
fun isEnabled (experiment : ToolkitExperiment ): Boolean =
58
102
EP_NAME .extensionList.contains(experiment) && enabledState.getOrDefault(experiment.id, getDefault(experiment))
59
103
60
104
fun setState (experiment : ToolkitExperiment , enabled : Boolean ) {
105
+ if (enabled != isEnabled(experiment)) {
106
+ ApplicationManager .getApplication().messageBus.syncPublisher(EXPERIMENT_CHANGED ).enableSettingsStateChanged(experiment)
107
+ }
61
108
if (enabled == getDefault(experiment)) {
62
109
enabledState.remove(experiment.id)
63
110
} else {
@@ -73,11 +120,11 @@ internal class ToolkitExperimentManager : PersistentStateComponent<ExperimentSta
73
120
)
74
121
}
75
122
76
- override fun getState (): ExperimentState = ExperimentState (). apply { value.putAll(enabledState) }
123
+ override fun getState (): ExperimentState = state
77
124
78
- override fun loadState (state : ExperimentState ) {
79
- enabledState.clear( )
80
- enabledState.putAll(state.value )
125
+ override fun loadState (loadedState : ExperimentState ) {
126
+ state.value.replace(loadedState.value )
127
+ state.nextSuggestion.replace(loadedState.nextSuggestion )
81
128
}
82
129
83
130
private fun getDefault (experiment : ToolkitExperiment ): Boolean {
@@ -89,14 +136,40 @@ internal class ToolkitExperimentManager : PersistentStateComponent<ExperimentSta
89
136
}
90
137
}
91
138
139
+ internal fun shouldPrompt (experiment : ToolkitExperiment , now : Instant = Instant .now()): Boolean {
140
+ if (experiment.isEnabled()) {
141
+ return false
142
+ }
143
+ val should = state.nextSuggestion[experiment.id]?.let { now.isAfter(Instant .ofEpochMilli(it)) } ? : true
144
+ if (should) {
145
+ state.nextSuggestion[experiment.id] = now.plus(experiment.suggestionSnooze).toEpochMilli()
146
+ }
147
+ return should
148
+ }
149
+
150
+ internal fun neverPrompt (experiment : ToolkitExperiment ) {
151
+ state.nextSuggestion[experiment.id] = Long .MAX_VALUE // This is ~240 years in the future, effectively "never".
152
+ }
153
+
92
154
companion object {
93
155
internal val EP_NAME = ExtensionPointName .create<ToolkitExperiment >(" aws.toolkit.experiment" )
156
+ internal val EXPERIMENT_CHANGED = Topic .create(" experiment service enable state changed" , ToolkitExperimentStateChangedListener ::class .java)
94
157
internal fun getInstance (): ToolkitExperimentManager = service()
95
158
internal fun visibleExperiments (): List <ToolkitExperiment > = EP_NAME .extensionList.filterNot { it.hidden }
96
159
}
97
160
}
98
161
99
162
internal class ExperimentState : BaseState () {
163
+ // This represents whether an experiment is enabled or not, don't want to rename it as that will
164
+ // cause problems with any experiments already out there in the wild who've been persisted
165
+ // as 'value'
100
166
@get:Property
101
167
val value by map<String , Boolean >()
168
+
169
+ @get:Property
170
+ val nextSuggestion by map<String , Long >()
171
+ }
172
+
173
+ interface ToolkitExperimentStateChangedListener {
174
+ fun enableSettingsStateChanged (toolkitExperiment : ToolkitExperiment )
102
175
}
0 commit comments