sidekiq 6.5.12 → 7.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +303 -20
  3. data/README.md +43 -35
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiq +3 -8
  6. data/bin/sidekiqload +204 -118
  7. data/bin/sidekiqmon +3 -0
  8. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +88 -0
  9. data/lib/generators/sidekiq/job_generator.rb +2 -0
  10. data/lib/sidekiq/api.rb +196 -138
  11. data/lib/sidekiq/capsule.rb +132 -0
  12. data/lib/sidekiq/cli.rb +60 -75
  13. data/lib/sidekiq/client.rb +87 -38
  14. data/lib/sidekiq/component.rb +4 -1
  15. data/lib/sidekiq/config.rb +305 -0
  16. data/lib/sidekiq/deploy.rb +64 -0
  17. data/lib/sidekiq/embedded.rb +63 -0
  18. data/lib/sidekiq/fetch.rb +11 -14
  19. data/lib/sidekiq/iterable_job.rb +55 -0
  20. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  21. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  22. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  23. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  24. data/lib/sidekiq/job/iterable.rb +294 -0
  25. data/lib/sidekiq/job.rb +382 -10
  26. data/lib/sidekiq/job_logger.rb +23 -12
  27. data/lib/sidekiq/job_retry.rb +42 -19
  28. data/lib/sidekiq/job_util.rb +53 -15
  29. data/lib/sidekiq/launcher.rb +71 -65
  30. data/lib/sidekiq/logger.rb +2 -27
  31. data/lib/sidekiq/manager.rb +9 -11
  32. data/lib/sidekiq/metrics/query.rb +9 -4
  33. data/lib/sidekiq/metrics/shared.rb +21 -9
  34. data/lib/sidekiq/metrics/tracking.rb +40 -26
  35. data/lib/sidekiq/middleware/chain.rb +19 -18
  36. data/lib/sidekiq/middleware/current_attributes.rb +70 -20
  37. data/lib/sidekiq/middleware/modules.rb +2 -0
  38. data/lib/sidekiq/monitor.rb +18 -4
  39. data/lib/sidekiq/paginator.rb +2 -2
  40. data/lib/sidekiq/processor.rb +62 -57
  41. data/lib/sidekiq/rails.rb +21 -10
  42. data/lib/sidekiq/redis_client_adapter.rb +31 -71
  43. data/lib/sidekiq/redis_connection.rb +44 -115
  44. data/lib/sidekiq/ring_buffer.rb +2 -0
  45. data/lib/sidekiq/scheduled.rb +22 -23
  46. data/lib/sidekiq/systemd.rb +2 -0
  47. data/lib/sidekiq/testing.rb +37 -46
  48. data/lib/sidekiq/transaction_aware_client.rb +11 -5
  49. data/lib/sidekiq/version.rb +6 -1
  50. data/lib/sidekiq/web/action.rb +15 -5
  51. data/lib/sidekiq/web/application.rb +89 -17
  52. data/lib/sidekiq/web/csrf_protection.rb +10 -7
  53. data/lib/sidekiq/web/helpers.rb +102 -42
  54. data/lib/sidekiq/web/router.rb +5 -2
  55. data/lib/sidekiq/web.rb +65 -17
  56. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  57. data/lib/sidekiq.rb +78 -274
  58. data/sidekiq.gemspec +12 -10
  59. data/web/assets/javascripts/application.js +44 -0
  60. data/web/assets/javascripts/base-charts.js +106 -0
  61. data/web/assets/javascripts/dashboard-charts.js +192 -0
  62. data/web/assets/javascripts/dashboard.js +11 -233
  63. data/web/assets/javascripts/metrics.js +151 -115
  64. data/web/assets/stylesheets/application-dark.css +4 -0
  65. data/web/assets/stylesheets/application-rtl.css +10 -89
  66. data/web/assets/stylesheets/application.css +53 -298
  67. data/web/locales/ar.yml +70 -70
  68. data/web/locales/cs.yml +62 -62
  69. data/web/locales/da.yml +60 -53
  70. data/web/locales/de.yml +65 -65
  71. data/web/locales/el.yml +2 -7
  72. data/web/locales/en.yml +78 -71
  73. data/web/locales/es.yml +68 -68
  74. data/web/locales/fa.yml +65 -65
  75. data/web/locales/fr.yml +80 -67
  76. data/web/locales/gd.yml +98 -0
  77. data/web/locales/he.yml +65 -64
  78. data/web/locales/hi.yml +59 -59
  79. data/web/locales/it.yml +53 -53
  80. data/web/locales/ja.yml +67 -70
  81. data/web/locales/ko.yml +52 -52
  82. data/web/locales/lt.yml +66 -66
  83. data/web/locales/nb.yml +61 -61
  84. data/web/locales/nl.yml +52 -52
  85. data/web/locales/pl.yml +45 -45
  86. data/web/locales/pt-br.yml +78 -69
  87. data/web/locales/pt.yml +51 -51
  88. data/web/locales/ru.yml +67 -66
  89. data/web/locales/sv.yml +53 -53
  90. data/web/locales/ta.yml +60 -60
  91. data/web/locales/tr.yml +100 -0
  92. data/web/locales/uk.yml +85 -61
  93. data/web/locales/ur.yml +64 -64
  94. data/web/locales/vi.yml +67 -67
  95. data/web/locales/zh-cn.yml +20 -19
  96. data/web/locales/zh-tw.yml +10 -2
  97. data/web/views/_footer.erb +17 -2
  98. data/web/views/_job_info.erb +18 -2
  99. data/web/views/_metrics_period_select.erb +12 -0
  100. data/web/views/_paging.erb +2 -0
  101. data/web/views/_poll_link.erb +1 -1
  102. data/web/views/_summary.erb +7 -7
  103. data/web/views/busy.erb +46 -35
  104. data/web/views/dashboard.erb +28 -7
  105. data/web/views/filtering.erb +7 -0
  106. data/web/views/layout.erb +6 -6
  107. data/web/views/metrics.erb +48 -26
  108. data/web/views/metrics_for_job.erb +43 -71
  109. data/web/views/morgue.erb +5 -9
  110. data/web/views/queue.erb +10 -14
  111. data/web/views/queues.erb +9 -3
  112. data/web/views/retries.erb +5 -9
  113. data/web/views/scheduled.erb +12 -13
  114. metadata +53 -39
  115. data/lib/sidekiq/delay.rb +0 -43
  116. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  117. data/lib/sidekiq/extensions/active_record.rb +0 -43
  118. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  119. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  120. data/lib/sidekiq/metrics/deploy.rb +0 -47
  121. data/lib/sidekiq/worker.rb +0 -370
  122. data/web/assets/javascripts/graph.js +0 -16
  123. /data/{LICENSE → LICENSE.txt} +0 -0
@@ -1,89 +1,66 @@
1
- if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
2
- Chart.defaults.borderColor = "#333"
3
- Chart.defaults.color = "#aaa"
4
- }
1
+ class JobMetricsOverviewChart extends BaseChart {
2
+ constructor(el, options) {
3
+ super(el, { ...options, chartType: "line" });
4
+ this.swatches = [];
5
+ this.visibleKls = options.visibleKls;
5
6
 
6
- class BaseChart {
7
- constructor(id, options) {
8
- this.ctx = document.getElementById(id);
9
- this.options = options
10
- this.fallbackColor = "#999";
11
- this.colors = [
12
- // Colors taken from https://www.chartjs.org/docs/latest/samples/utils.html
13
- "#537bc4",
14
- "#4dc9f6",
15
- "#f67019",
16
- "#f53794",
17
- "#acc236",
18
- "#166a8f",
19
- "#00a950",
20
- "#58595b",
21
- "#8549ba",
22
- "#991b1b",
23
- ];
7
+ const countBuckets = this.options.labels.length / 60;
8
+ this.labelBuckets = this.options.labels.reduce((acc, label, index) => {
9
+ const bucket = Math.floor(index / countBuckets);
10
+ acc[bucket] = acc[bucket] || [];
11
+ acc[bucket].push(label);
12
+ return acc;
13
+ }, []);
24
14
 
25
- this.chart = new Chart(this.ctx, {
26
- type: this.options.chartType,
27
- data: { labels: this.options.labels, datasets: this.datasets },
28
- options: this.chartOptions,
29
- });
15
+ this.init();
30
16
  }
31
17
 
32
- addMarksToChart() {
33
- this.options.marks.forEach(([bucket, label], i) => {
34
- this.chart.options.plugins.annotation.annotations[`deploy-${i}`] = {
35
- type: "line",
36
- xMin: bucket,
37
- xMax: bucket,
38
- borderColor: "rgba(220, 38, 38, 0.4)",
39
- borderWidth: 2,
40
- };
41
- });
18
+ get datasets() {
19
+ return Object.entries(this.options.series)
20
+ .filter(([kls, _]) => this.visibleKls.includes(kls))
21
+ .map(([kls, _]) => this.buildDataset(kls));
42
22
  }
43
- }
44
23
 
45
- class JobMetricsOverviewChart extends BaseChart {
46
- constructor(id, options) {
47
- super(id, { ...options, chartType: "line" });
48
- this.swatches = [];
24
+ get metric() {
25
+ return this._metric || this.options.initialMetric;
26
+ }
49
27
 
50
- this.addMarksToChart();
51
- this.chart.update();
28
+ set metric(m) {
29
+ this._metric = m;
52
30
  }
53
31
 
54
32
  registerSwatch(id) {
55
33
  const el = document.getElementById(id);
56
- el.onchange = () => this.toggle(el.value, el.checked);
34
+ el.addEventListener("change", () => this.toggleKls(el.value, el.checked));
57
35
  this.swatches[el.value] = el;
58
- this.updateSwatch(el.value);
36
+ this.updateSwatch(el.value, el.checked);
59
37
  }
60
38
 
61
- updateSwatch(kls) {
39
+ updateSwatch(kls, checked) {
62
40
  const el = this.swatches[kls];
63
- const ds = this.chart.data.datasets.find((ds) => ds.label == kls);
64
- el.checked = !!ds;
65
- el.style.color = ds ? ds.borderColor : null;
41
+ el.checked = checked;
42
+ el.style.color = this.colors.assignments[kls] || "";
66
43
  }
67
44
 
68
- toggle(kls, visible) {
45
+ toggleKls(kls, visible) {
69
46
  if (visible) {
70
- this.chart.data.datasets.push(this.dataset(kls));
47
+ this.chart.data.datasets.push(this.buildDataset(kls));
71
48
  } else {
72
49
  const i = this.chart.data.datasets.findIndex((ds) => ds.label == kls);
73
- this.colors.unshift(this.chart.data.datasets[i].borderColor);
50
+ this.colors.checkIn(kls);
74
51
  this.chart.data.datasets.splice(i, 1);
75
52
  }
76
53
 
77
- this.updateSwatch(kls);
78
- this.chart.update();
54
+ this.updateSwatch(kls, visible);
55
+ this.update();
79
56
  }
80
57
 
81
- dataset(kls) {
82
- const color = this.colors.shift() || this.fallbackColor;
58
+ buildDataset(kls) {
59
+ const color = this.colors.checkOut(kls);
83
60
 
84
61
  return {
85
62
  label: kls,
86
- data: this.options.series[kls],
63
+ data: this.buildSeries(kls),
87
64
  borderColor: color,
88
65
  backgroundColor: color,
89
66
  borderWidth: 2,
@@ -91,40 +68,56 @@ class JobMetricsOverviewChart extends BaseChart {
91
68
  };
92
69
  }
93
70
 
94
- get datasets() {
95
- return Object.entries(this.options.series)
96
- .filter(([kls, _]) => this.options.visible.includes(kls))
97
- .map(([kls, _]) => this.dataset(kls));
71
+ buildSeries(kls) {
72
+ // `series` is an object that maps labels to counts => { "20:15" => 2, "20:16" => 3, ... }
73
+ const series = this.options.series[kls];
74
+ return this.labelBuckets.reduce((acc, labels) => {
75
+ const bucketValues = labels.map(label => series[label]).filter(v => v);
76
+ if (bucketValues.length > 0) {
77
+ // Sum up the values for each bucket that has data.
78
+ // The new label is the bucket's first label, its start time.
79
+ acc[labels[0]] = bucketValues.reduce((a, b) => a + b, 0);
80
+ }
81
+ return acc;
82
+ }, {});
83
+ }
84
+
85
+ buildTooltipTitle(items) {
86
+ const [first, ...rest] = this.labelBuckets.find((labels) => labels[0] === items[0].label);
87
+ const title = [first, rest[rest.length - 1]].filter(v => v).join(" - ");
88
+ return `${title} UTC`
98
89
  }
99
90
 
100
91
  get chartOptions() {
101
92
  return {
93
+ ...super.chartOptions,
102
94
  aspectRatio: 4,
103
95
  scales: {
96
+ ...super.chartOptions.scales,
104
97
  y: {
98
+ ...super.chartOptions.scales.y,
105
99
  beginAtZero: true,
106
100
  title: {
107
- text: "Total execution time (sec)",
101
+ text: this.options.yLabel,
108
102
  display: true,
109
103
  },
110
104
  },
111
105
  },
112
- interaction: {
113
- mode: "x",
114
- },
115
106
  plugins: {
116
- legend: {
117
- display: false,
118
- },
107
+ ...super.chartOptions.plugins,
119
108
  tooltip: {
109
+ ...super.chartOptions.plugins.tooltip,
120
110
  callbacks: {
121
- title: (items) => `${items[0].label} UTC`,
111
+ title: (items) => this.buildTooltipTitle(items),
122
112
  label: (item) =>
123
- `${item.dataset.label}: ${item.parsed.y.toFixed(1)} seconds`,
113
+ `${item.dataset.label}: ${item.parsed.y.toFixed(1)} ` +
114
+ `${this.options.units}`,
124
115
  footer: (items) => {
125
116
  const bucket = items[0].label;
126
117
  const marks = this.options.marks.filter(([b, _]) => b == bucket);
127
- return marks.map(([b, msg]) => `Deploy: ${msg}`);
118
+ return marks.map(
119
+ ([b, msg]) => `${this.options.markLabel}: ${msg}`
120
+ );
128
121
  },
129
122
  },
130
123
  },
@@ -134,40 +127,49 @@ class JobMetricsOverviewChart extends BaseChart {
134
127
  }
135
128
 
136
129
  class HistTotalsChart extends BaseChart {
137
- constructor(id, options) {
138
- super(id, { ...options, chartType: "bar" });
130
+ constructor(el, options) {
131
+ super(el, { ...options, chartType: "bar" });
132
+ this.init();
139
133
  }
140
134
 
141
135
  get datasets() {
142
- return [{
143
- data: this.options.series,
144
- backgroundColor: this.colors[0],
145
- borderWidth: 0,
146
- }];
136
+ return [
137
+ {
138
+ data: this.options.series,
139
+ backgroundColor: this.colors.primary,
140
+ borderWidth: 0,
141
+ },
142
+ ];
147
143
  }
148
144
 
149
145
  get chartOptions() {
150
146
  return {
147
+ ...super.chartOptions,
151
148
  aspectRatio: 6,
152
149
  scales: {
150
+ ...super.chartOptions.scales,
153
151
  y: {
152
+ ...super.chartOptions.scales.y,
154
153
  beginAtZero: true,
155
154
  title: {
156
- text: "Total jobs",
155
+ text: this.options.yLabel,
156
+ display: true,
157
+ },
158
+ },
159
+ x: {
160
+ ...super.chartOptions.scales.x,
161
+ title: {
162
+ text: this.options.xLabel,
157
163
  display: true,
158
164
  },
159
165
  },
160
- },
161
- interaction: {
162
- mode: "x",
163
166
  },
164
167
  plugins: {
165
- legend: {
166
- display: false,
167
- },
168
+ ...super.chartOptions.plugins,
168
169
  tooltip: {
170
+ ...super.chartOptions.plugins.tooltip,
169
171
  callbacks: {
170
- label: (item) => `${item.parsed.y} jobs`,
172
+ label: (item) => `${item.parsed.y} ${this.options.units}`,
171
173
  },
172
174
  },
173
175
  },
@@ -176,11 +178,9 @@ class HistTotalsChart extends BaseChart {
176
178
  }
177
179
 
178
180
  class HistBubbleChart extends BaseChart {
179
- constructor(id, options) {
180
- super(id, { ...options, chartType: "bubble" });
181
-
182
- this.addMarksToChart();
183
- this.chart.update();
181
+ constructor(el, options) {
182
+ super(el, { ...options, chartType: "bubble" });
183
+ this.init();
184
184
  }
185
185
 
186
186
  get datasets() {
@@ -190,13 +190,13 @@ class HistBubbleChart extends BaseChart {
190
190
  Object.entries(this.options.hist).forEach(([bucket, hist]) => {
191
191
  hist.forEach((count, histBucket) => {
192
192
  if (count > 0) {
193
+ // histogram data is ordered fastest to slowest, but this.histIntervals is
194
+ // slowest to fastest (so it displays correctly on the chart).
195
+ const index = this.options.histIntervals.length - 1 - histBucket;
196
+
193
197
  data.push({
194
198
  x: bucket,
195
- // histogram data is ordered fastest to slowest, but this.histIntervals is
196
- // slowest to fastest (so it displays correctly on the chart).
197
- y:
198
- this.options.histIntervals[this.options.histIntervals.length - 1 - histBucket] /
199
- 1000,
199
+ y: this.options.histIntervals[index] / 1000,
200
200
  count: count,
201
201
  });
202
202
 
@@ -206,53 +206,55 @@ class HistBubbleChart extends BaseChart {
206
206
  });
207
207
 
208
208
  // Chart.js will not calculate the bubble size. We have to do that.
209
- const maxRadius = this.ctx.offsetWidth / this.options.labels.length;
210
- const minRadius = 1
209
+ const maxRadius = this.el.offsetWidth / this.options.labels.length;
210
+ const minRadius = 1;
211
211
  const multiplier = (maxRadius / maxCount) * 1.5;
212
212
  data.forEach((entry) => {
213
213
  entry.r = entry.count * multiplier + minRadius;
214
214
  });
215
215
 
216
- return [{
217
- data: data,
218
- backgroundColor: "#537bc4",
219
- borderColor: "#537bc4",
220
- }];
216
+ return [
217
+ {
218
+ data: data,
219
+ backgroundColor: this.colors.primary,
220
+ borderColor: this.colors.primary,
221
+ },
222
+ ];
221
223
  }
222
224
 
223
225
  get chartOptions() {
224
226
  return {
227
+ ...super.chartOptions,
225
228
  aspectRatio: 3,
226
229
  scales: {
230
+ ...super.chartOptions.scales,
227
231
  x: {
232
+ ...super.chartOptions.scales.x,
228
233
  type: "category",
229
234
  labels: this.options.labels,
230
235
  },
231
236
  y: {
237
+ ...super.chartOptions.scales.y,
232
238
  title: {
233
- text: "Execution time (sec)",
239
+ text: this.options.yLabel,
234
240
  display: true,
235
241
  },
236
242
  },
237
243
  },
238
- interaction: {
239
- mode: "x",
240
- },
241
244
  plugins: {
242
- legend: {
243
- display: false,
244
- },
245
+ ...super.chartOptions.plugins,
245
246
  tooltip: {
247
+ ...super.chartOptions.plugins.tooltip,
246
248
  callbacks: {
247
249
  title: (items) => `${items[0].raw.x} UTC`,
248
250
  label: (item) =>
249
- `${item.parsed.y} seconds: ${item.raw.count} job${
250
- item.raw.count == 1 ? "" : "s"
251
- }`,
251
+ `${item.parsed.y} ${this.options.yUnits}: ${item.raw.count} ${this.options.zUnits}`,
252
252
  footer: (items) => {
253
253
  const bucket = items[0].raw.x;
254
254
  const marks = this.options.marks.filter(([b, _]) => b == bucket);
255
- return marks.map(([b, msg]) => `Deploy: ${msg}`);
255
+ return marks.map(
256
+ ([b, msg]) => `${this.options.markLabel}: ${msg}`
257
+ );
256
258
  },
257
259
  },
258
260
  },
@@ -260,3 +262,37 @@ class HistBubbleChart extends BaseChart {
260
262
  };
261
263
  }
262
264
  }
265
+
266
+ var ch = document.getElementById("job-metrics-overview-chart");
267
+ if (ch != null) {
268
+ var jm = new JobMetricsOverviewChart(ch, JSON.parse(ch.textContent));
269
+ document.querySelectorAll(".metrics-swatch-wrapper > input[type=checkbox]").forEach((imp) => {
270
+ jm.registerSwatch(imp.id)
271
+ });
272
+ window.jobMetricsChart = jm;
273
+ }
274
+
275
+ var htc = document.getElementById("hist-totals-chart");
276
+ if (htc != null) {
277
+ var tc = new HistTotalsChart(htc, JSON.parse(htc.textContent));
278
+ window.histTotalsChart = tc
279
+ }
280
+
281
+ var hbc = document.getElementById("hist-bubble-chart");
282
+ if (hbc != null) {
283
+ var bc = new HistBubbleChart(hbc, JSON.parse(hbc.textContent));
284
+ window.histBubbleChart = bc
285
+ }
286
+
287
+ var form = document.getElementById("metrics-form")
288
+ document.querySelectorAll("#period-selector").forEach(node => {
289
+ node.addEventListener("input", debounce(() => form.submit()))
290
+ })
291
+
292
+ function debounce(func, timeout = 300) {
293
+ let timer;
294
+ return (...args) => {
295
+ clearTimeout(timer);
296
+ timer = setTimeout(() => { func.apply(this, args); }, timeout);
297
+ };
298
+ }
@@ -72,6 +72,10 @@ table {
72
72
  background-color: #31708f;
73
73
  }
74
74
 
75
+ .alert-warning {
76
+ background-color: #c47612;
77
+ }
78
+
75
79
  a:link, a:active, a:hover, a:visited {
76
80
  color: #ddd;
77
81
  }
@@ -102,22 +102,8 @@ div.interval-slider {
102
102
  float: left;
103
103
  }
104
104
 
105
- #realtime-legend,
106
- #history-legend {
107
- text-align: right;
108
- float: left;
109
- }
110
- #realtime-legend .timestamp,
111
- #history-legend .timestamp {
112
- text-align: left;
113
- }
114
- #realtime-legend .line,
115
- #history-legend .line {
116
- margin: 0 20px 0 0;
117
- }
118
- #realtime-legend .swatch,
119
- #history-legend .swatch {
120
- margin: 0 0 0 8px;
105
+ #realtime-legend {
106
+ justify-content: start;
121
107
  }
122
108
 
123
109
  /* Beacon
@@ -166,77 +152,12 @@ div.interval-slider {
166
152
  }
167
153
  }
168
154
 
169
- /* Rickshaw */
170
-
171
- .rickshaw_graph .detail .x_label.left {
172
- right: 0
173
- }
174
- .rickshaw_graph .detail .x_label.right {
175
- left: 0
176
- }
177
- .rickshaw_graph .detail .item.left {
178
- right: 0
179
- }
180
- .rickshaw_graph .detail .item.right {
181
- left: 0
182
- }
183
- .rickshaw_graph .detail .item.left:after {
184
- left: 0;
185
- right: -5px;
186
- border-right-color: unset;
187
- border-left-color: rgba(0, 0, 0, .8);
188
- border-right-width: 0;
189
- border-left-width: unset;
190
- }
191
- .rickshaw_graph .detail .item.right:after {
192
- right: 0;
193
- left: -5px;
194
- border-left-color: unset;
195
- border-right-color: rgba(0, 0, 0, .8);
196
- border-left-width: 0;
197
- border-right-width: unset;
198
- }
199
- .rickshaw_graph .detail .dot {
200
- margin-right: -3px;
201
- margin-left: unset;
202
- }
203
- .rickshaw_graph .x_tick {
204
- border-left: unset;
205
- border-right: 1px dotted rgba(0, 0, 0, .2);
206
- }
207
- .rickshaw_graph .x_tick .title {
208
- margin-right: 3px;
209
- margin-left: unset;
210
- }
211
- .rickshaw_annotation_timeline .annotation {
212
- margin-right: -2px;
213
- margin-left: unset;
214
- }
215
- .rickshaw_graph .annotation_line {
216
- border-right: 2px solid rgba(0, 0, 0, .3);
217
- border-left: unset;
218
- }
219
- .rickshaw_annotation_timeline .annotation .content {
220
- left: unset;
221
- right: -11px;
222
- }
223
- .rickshaw_graph .x_tick.glow .title,
224
- .rickshaw_graph .y_ticks.glow text {
225
- text-shadow: 1px 1px 0 rgba(255, 255, 255, .1), -1px -1px 0 rgba(255, 255, 255, .1), -1px 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1), 0 -1px 0 rgba(255, 255, 255, .1), -1px 0 0 rgba(255, 255, 255, .1), 1px 0 0 rgba(255, 255, 255, .1), 1px -1px 0 rgba(255, 255, 255, .1)
226
- }
227
- .rickshaw_graph .x_tick.inverse .title,
228
- .rickshaw_graph .y_ticks.inverse text {
229
- text-shadow: 1px 1px 0 rgba(0, 0, 0, .8), -1px -1px 0 rgba(0, 0, 0, .8), -1px 1px 0 rgba(0, 0, 0, .8), 0 1px 0 rgba(0, 0, 0, .8), 0 -1px 0 rgba(0, 0, 0, .8), -1px 0 0 rgba(0, 0, 0, .8), 1px 0 0 rgba(0, 0, 0, .8), 1px -1px 0 rgba(0, 0, 0, .8)
230
- }
231
- .rickshaw_legend .line {
232
- padding-left: 15px;
233
- padding-right: unset;
234
- }
235
- .rickshaw_legend .line .swatch {
236
- margin-left: 3px;
237
- margin-right: unset;
238
- }
239
- .rickshaw_legend .action {
240
- margin-left: .2em;
241
- margin-right: unset;
155
+ #locale-select {
156
+ float: right;
242
157
  }
158
+
159
+ @media (max-width: 767px) {
160
+ #locale-select {
161
+ float: none;
162
+ }
163
+ }