mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-10 15:38:19 -05:00
Compare commits
5869 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07af5f91c8 | ||
|
|
fda6a9fede | ||
|
|
d2245a2e34 | ||
|
|
a12a8130b8 | ||
|
|
b436d2a96d | ||
|
|
05b0f270c4 | ||
|
|
f24f71e4e0 | ||
|
|
63a00f775f | ||
|
|
5a946caf35 | ||
|
|
16c016a171 | ||
|
|
284af65534 | ||
|
|
b5da7868fa | ||
|
|
c8a41aa834 | ||
|
|
390f6d3b95 | ||
|
|
9a2140cd19 | ||
|
|
e3cf4fb089 | ||
|
|
472a1cdba1 | ||
|
|
b56c275364 | ||
|
|
592e825a0f | ||
|
|
50a140c580 | ||
|
|
48d255a235 | ||
|
|
a01fcc5194 | ||
|
|
b6e9e9d216 | ||
|
|
5ddb99a5b4 | ||
|
|
f981ed6363 | ||
|
|
6d6b0ea6f9 | ||
|
|
0e355ed25c | ||
|
|
80a9ff470c | ||
|
|
ead0abcd62 | ||
|
|
7dcb54bc7e | ||
|
|
ae82850e1f | ||
|
|
0610d756a8 | ||
|
|
6325067130 | ||
|
|
1a4e52dc73 | ||
|
|
08383ad8b9 | ||
|
|
ecf6bfe9a7 | ||
|
|
f7528568e5 | ||
|
|
51f653b66f | ||
|
|
460a93a5fd | ||
|
|
3ea4c8eb1d | ||
|
|
f237de559d | ||
|
|
8c7abbac44 | ||
|
|
1710316142 | ||
|
|
2d856f4f09 | ||
|
|
ab219e3408 | ||
|
|
673be3dd77 | ||
|
|
b25e6feabb | ||
|
|
e6b22d86b6 | ||
|
|
9c5196ee63 | ||
|
|
9b53251b09 | ||
|
|
942bdd8c40 | ||
|
|
d762865753 | ||
|
|
992b967095 | ||
|
|
9a39cea0cc | ||
|
|
db9f210257 | ||
|
|
e8b7c08a19 | ||
|
|
b596245571 | ||
|
|
57bb3a12d3 | ||
|
|
219fbc6819 | ||
|
|
a660093fa5 | ||
|
|
eac0eea886 | ||
|
|
6df0bb9423 | ||
|
|
159e771ac7 | ||
|
|
ca41c0831b | ||
|
|
316c3c8409 | ||
|
|
f79a22b987 | ||
|
|
83219e3551 | ||
|
|
f55c26af4a | ||
|
|
9fbb2c5829 | ||
|
|
531f31b66f | ||
|
|
58e152154a | ||
|
|
55b37fdeb3 | ||
|
|
b8a5aef53a | ||
|
|
d764ea9b4e | ||
|
|
8ad779f949 | ||
|
|
7642fc84b7 | ||
|
|
0952f3e0a7 | ||
|
|
13ca8dfc57 | ||
|
|
79f42f5846 | ||
|
|
9a35b01e23 | ||
|
|
5bf0701939 | ||
|
|
e5083f4938 | ||
|
|
ce6a562a3c | ||
|
|
b6bcfc8683 | ||
|
|
d37beeece7 | ||
|
|
79ee25620f | ||
|
|
82c678a1eb | ||
|
|
458924569a | ||
|
|
501b012b2b | ||
|
|
ee1aac847a | ||
|
|
7785a76a67 | ||
|
|
79af1d6c92 | ||
|
|
a145add5d4 | ||
|
|
abf71389be | ||
|
|
2e2114f99d | ||
|
|
09d415502f | ||
|
|
c489fef4ff | ||
|
|
9d12b0fca7 | ||
|
|
cc9b9d4e6a | ||
|
|
34f9e4484d | ||
|
|
03a3f8f886 | ||
|
|
b900db0c74 | ||
|
|
5fb3ebc2d9 | ||
|
|
fbc6b2df57 | ||
|
|
6208225354 | ||
|
|
e22b693418 | ||
|
|
c7ca49c634 | ||
|
|
d84aa94762 | ||
|
|
89e2c18531 | ||
|
|
43d36c08dc | ||
|
|
4bc330e5dd | ||
|
|
56ab530d87 | ||
|
|
599209a036 | ||
|
|
15b5fa6c01 | ||
|
|
3f4676d9a6 | ||
|
|
bb89cf9235 | ||
|
|
25c40967da | ||
|
|
068b232e75 | ||
|
|
0b7fda707e | ||
|
|
9fd4ba199e | ||
|
|
43e40c7969 | ||
|
|
248f19333c | ||
|
|
306c4f847b | ||
|
|
e689cdc522 | ||
|
|
3e0efd8484 | ||
|
|
4a8400d2a5 | ||
|
|
e432bb0592 | ||
|
|
d002734afe | ||
|
|
54e7377f24 | ||
|
|
a49d24108f | ||
|
|
733a4e8983 | ||
|
|
6309a6fca3 | ||
|
|
3d66940402 | ||
|
|
2872c87e32 | ||
|
|
ecb591f2e1 | ||
|
|
8e37d45948 | ||
|
|
c11f120bb9 | ||
|
|
950c63049b | ||
|
|
3edf9bfad8 | ||
|
|
420a2c054c | ||
|
|
5e566a057d | ||
|
|
edd1db78e3 | ||
|
|
47526a769a | ||
|
|
0474eeeead | ||
|
|
e8cdc78f0f | ||
|
|
7742b652ba | ||
|
|
7ed68ed927 | ||
|
|
ac06350e08 | ||
|
|
43698f0860 | ||
|
|
c73e1a21b8 | ||
|
|
d855468398 | ||
|
|
b8efd88448 | ||
|
|
a8c6065b9f | ||
|
|
12a8b228d9 | ||
|
|
47fb562151 | ||
|
|
f0059b71e5 | ||
|
|
89aaa977e8 | ||
|
|
ae308e03ad | ||
|
|
c1a1542cfe | ||
|
|
3c4a908773 | ||
|
|
e2b7f91138 | ||
|
|
c73978cdd5 | ||
|
|
6760c31818 | ||
|
|
781ceaaa5c | ||
|
|
fff283648b | ||
|
|
f71f1256b8 | ||
|
|
f26b1335d8 | ||
|
|
370ba1f288 | ||
|
|
54988c17c8 | ||
|
|
65fe23fa02 | ||
|
|
b22b06e3d2 | ||
|
|
3677f4e1c6 | ||
|
|
d6eb0493b3 | ||
|
|
4e13cdb365 | ||
|
|
4e3710cdaa | ||
|
|
5feedaf4c8 | ||
|
|
ce29695e44 | ||
|
|
f0f06a2c34 | ||
|
|
860a3fef86 | ||
|
|
6afadade5d | ||
|
|
c1bda7fb7b | ||
|
|
509c165ee8 | ||
|
|
54e0ef7598 | ||
|
|
03f2d5f014 | ||
|
|
122c1e74cd | ||
|
|
d29f4cdf21 | ||
|
|
300fb217d1 | ||
|
|
33dc41c4bd | ||
|
|
4b6e257dfc | ||
|
|
1f23f37f89 | ||
|
|
59b1c46485 | ||
|
|
01705c1299 | ||
|
|
18955a2abf | ||
|
|
dbbe7f9679 | ||
|
|
61a51a869f | ||
|
|
298822fc44 | ||
|
|
283c71e772 | ||
|
|
b1297395a9 | ||
|
|
b850cd2509 | ||
|
|
56878ebbaa | ||
|
|
6a09fda08e | ||
|
|
77bc4e9ec7 | ||
|
|
a1b9fda809 | ||
|
|
97a12ec601 | ||
|
|
81bdde64c4 | ||
|
|
c4ef42337b | ||
|
|
ce78d0dcf1 | ||
|
|
7069064dd6 | ||
|
|
a0a83d5fe3 | ||
|
|
8fbc4ad4c1 | ||
|
|
5346a00373 | ||
|
|
2dc69aea37 | ||
|
|
be6f6716bf | ||
|
|
7f59f93692 | ||
|
|
798b336df6 | ||
|
|
bef6b4bfd9 | ||
|
|
80ab088e2d | ||
|
|
481a692b07 | ||
|
|
8ae936ba01 | ||
|
|
6dc377842f | ||
|
|
837307fe8c | ||
|
|
dfe359c1b6 | ||
|
|
b8923e9497 | ||
|
|
9ebcbf8a2d | ||
|
|
a6fc6bf8cb | ||
|
|
eddc3047d4 | ||
|
|
6db17d4f67 | ||
|
|
d21f9493fb | ||
|
|
e87f942e89 | ||
|
|
1eb9ed7345 | ||
|
|
9d923d079a | ||
|
|
b054e45402 | ||
|
|
8b1d30aefe | ||
|
|
ab87a0708d | ||
|
|
0b97ee1ecc | ||
|
|
68546c319e | ||
|
|
69188fef22 | ||
|
|
6be6dfbfeb | ||
|
|
6fc06281bd | ||
|
|
f724128d41 | ||
|
|
518ef0f9f8 | ||
|
|
f5b395485e | ||
|
|
31aca7aa70 | ||
|
|
b38a630224 | ||
|
|
1b7709411b | ||
|
|
2b48aa0d91 | ||
|
|
6fdf23752e | ||
|
|
8542f5d017 | ||
|
|
e7b830755a | ||
|
|
23c7b0683a | ||
|
|
5805895581 | ||
|
|
90539bff25 | ||
|
|
62bb21d5b6 | ||
|
|
4f85268d44 | ||
|
|
611e517bb8 | ||
|
|
4d05a568c1 | ||
|
|
1330325519 | ||
|
|
92d850efa6 | ||
|
|
7bf56125eb | ||
|
|
69f861cc8a | ||
|
|
a94be7b128 | ||
|
|
703766fb2e | ||
|
|
8e73749e21 | ||
|
|
dc8ffa115b | ||
|
|
a0385da0d7 | ||
|
|
a6b7dee4cd | ||
|
|
f9ff5c2637 | ||
|
|
6a1fbc57f4 | ||
|
|
38337a926f | ||
|
|
bf6b87886c | ||
|
|
33e6b0a305 | ||
|
|
cb5eac052f | ||
|
|
39fee3782f | ||
|
|
654f30a312 | ||
|
|
a763da2b94 | ||
|
|
0c177d30dc | ||
|
|
20f8a181d4 | ||
|
|
eebcf70bb9 | ||
|
|
e7d62f55ca | ||
|
|
3a4e5f3982 | ||
|
|
3381c3a6b9 | ||
|
|
e42c920587 | ||
|
|
4635665394 | ||
|
|
08ff066fa3 | ||
|
|
816072fc29 | ||
|
|
5eb442aa03 | ||
|
|
c32db50585 | ||
|
|
1d9dde52df | ||
|
|
6176f03ad0 | ||
|
|
0fc1be52eb | ||
|
|
f12334e839 | ||
|
|
ffb6736f11 | ||
|
|
f084d3a34f | ||
|
|
0fe7cad876 | ||
|
|
ecff638920 | ||
|
|
db2ad780c0 | ||
|
|
5cd848bd28 | ||
|
|
ed6022e3f2 | ||
|
|
a063540407 | ||
|
|
aa932532f3 | ||
|
|
8160f8c726 | ||
|
|
af900d1037 | ||
|
|
49320e6edd | ||
|
|
4308887296 | ||
|
|
dffd8bc7fd | ||
|
|
554f835293 | ||
|
|
50e7b6768d | ||
|
|
db222af7e0 | ||
|
|
5b688707b7 | ||
|
|
8dfd8f5b33 | ||
|
|
20cc8e26b5 | ||
|
|
e48e9c6904 | ||
|
|
87c673f283 | ||
|
|
dfaefa8905 | ||
|
|
cd4f406234 | ||
|
|
190b542613 | ||
|
|
73e48ab5f4 | ||
|
|
62a95b9ce2 | ||
|
|
999eb1de3c | ||
|
|
55985740de | ||
|
|
e82b71da65 | ||
|
|
9ce1e6c549 | ||
|
|
cda59da698 | ||
|
|
51da80d381 | ||
|
|
412606c594 | ||
|
|
da2df5ac58 | ||
|
|
98f17e0cd2 | ||
|
|
9b2c04f298 | ||
|
|
ed4a97154d | ||
|
|
9dcd62f944 | ||
|
|
213c645524 | ||
|
|
f1c0ac2b30 | ||
|
|
16c6d55f60 | ||
|
|
6e40a3dd39 | ||
|
|
04ad1453c1 | ||
|
|
80eeff68ab | ||
|
|
6128a3ab65 | ||
|
|
5f25537079 | ||
|
|
c21ba0baca | ||
|
|
ebf786c2eb | ||
|
|
b39ffafea9 | ||
|
|
4441945a68 | ||
|
|
cd95e058e6 | ||
|
|
a0144d77ef | ||
|
|
64cec08a74 | ||
|
|
7a77b55e6a | ||
|
|
54e5a6b43c | ||
|
|
9653cc2893 | ||
|
|
47dc5b4116 | ||
|
|
9db9ef527a | ||
|
|
9285a1f7fc | ||
|
|
d22eb1524a | ||
|
|
5e3971c776 | ||
|
|
dbe4896102 | ||
|
|
ff5ef52dd5 | ||
|
|
634b1c7c3c | ||
|
|
1c9f8940a9 | ||
|
|
d4527cdc28 | ||
|
|
514206850a | ||
|
|
809bc8c6de | ||
|
|
0d720616f3 | ||
|
|
0c325d422f | ||
|
|
d368aa72c3 | ||
|
|
27e1348dcb | ||
|
|
474f14003b | ||
|
|
8a5da25545 | ||
|
|
540d68ed9f | ||
|
|
1ad49b153c | ||
|
|
15885cbb5f | ||
|
|
b6979d135c | ||
|
|
d4b19820a4 | ||
|
|
8d529095a7 | ||
|
|
77f9052ab5 | ||
|
|
9fcc1fe8ad | ||
|
|
5d30b55372 | ||
|
|
78d77ce373 | ||
|
|
2cae04186b | ||
|
|
59bddea1e9 | ||
|
|
d6bd3d9f9b | ||
|
|
8eb7b056f2 | ||
|
|
40ae9a903e | ||
|
|
5b6c80d4b2 | ||
|
|
fd7518018c | ||
|
|
5c03326a8c | ||
|
|
2c82ee0235 | ||
|
|
ac84be956a | ||
|
|
0fb154cc1e | ||
|
|
837e8d33f6 | ||
|
|
3365bfc395 | ||
|
|
135fe21278 | ||
|
|
fc7e237000 | ||
|
|
de8134742d | ||
|
|
f814034835 | ||
|
|
6d9fad29f3 | ||
|
|
313ae9044d | ||
|
|
9389160af0 | ||
|
|
6a7ffeff68 | ||
|
|
bc6ff57cb7 | ||
|
|
b52b9e4520 | ||
|
|
fb71ab6146 | ||
|
|
d8c7a634ff | ||
|
|
396dd2632e | ||
|
|
c11875a5f0 | ||
|
|
2e599faf1f | ||
|
|
a5a8385420 | ||
|
|
33ed698e5b | ||
|
|
ed547fdf40 | ||
|
|
0b1c35c92b | ||
|
|
d304bdf333 | ||
|
|
a9255e6b12 | ||
|
|
45429fb798 | ||
|
|
1206d1d3ba | ||
|
|
af44a447a1 | ||
|
|
d7fb6d1793 | ||
|
|
cb0083adb2 | ||
|
|
77bae68f26 | ||
|
|
e9f8b4d552 | ||
|
|
aee6b74cfb | ||
|
|
29b6b03297 | ||
|
|
b5ebd8a645 | ||
|
|
356ba8c5a1 | ||
|
|
5aa5cb1794 | ||
|
|
09c609e417 | ||
|
|
10e7d11846 | ||
|
|
4e442312a7 | ||
|
|
8ea97df3f2 | ||
|
|
a8c14dab96 | ||
|
|
00775b90e8 | ||
|
|
c6ae4c0034 | ||
|
|
1efcaa0b5d | ||
|
|
e6d94f6f66 | ||
|
|
5a8a02626c | ||
|
|
c23293750d | ||
|
|
9de74048a2 | ||
|
|
0fc861afb4 | ||
|
|
2300dae10b | ||
|
|
dfbb98abfd | ||
|
|
9670839235 | ||
|
|
1e2326c2b6 | ||
|
|
6f46b847e2 | ||
|
|
8c44559a1f | ||
|
|
d245a972ee | ||
|
|
c153d0591f | ||
|
|
2ce18ef324 | ||
|
|
2792d65e40 | ||
|
|
7b00f605aa | ||
|
|
4cb3f05e8e | ||
|
|
8e40c815eb | ||
|
|
6dfd8bc0ff | ||
|
|
2ef397f024 | ||
|
|
94749f6114 | ||
|
|
4a39588d00 | ||
|
|
f5d5832149 | ||
|
|
68deab9b0e | ||
|
|
96efac2bd7 | ||
|
|
a67059f9f9 | ||
|
|
cdc51c09c7 | ||
|
|
a6f02cf214 | ||
|
|
7600770a6e | ||
|
|
1b3ed88bd1 | ||
|
|
f01dc9f0fb | ||
|
|
9a65890e92 | ||
|
|
ffef0c2eb1 | ||
|
|
8b74338b0f | ||
|
|
ed3c676548 | ||
|
|
30c0a2bd29 | ||
|
|
997e5e5ce5 | ||
|
|
aba6b2e4db | ||
|
|
3f48e3b0af | ||
|
|
b4879a342c | ||
|
|
5070146061 | ||
|
|
d28c3e15ad | ||
|
|
4b496be520 | ||
|
|
979288a590 | ||
|
|
9365d4f3c4 | ||
|
|
e1daf8aae7 | ||
|
|
8a57dbf608 | ||
|
|
ac9b76c651 | ||
|
|
139526ef2d | ||
|
|
d1e7e6c63a | ||
|
|
29b574bf94 | ||
|
|
7e4248bbca | ||
|
|
fee267dc2e | ||
|
|
9864130435 | ||
|
|
c9770cbd03 | ||
|
|
ed6a40ba41 | ||
|
|
3c22cdf90c | ||
|
|
e55749b897 | ||
|
|
88cd87a5f7 | ||
|
|
244fdc72bc | ||
|
|
b9d163a71d | ||
|
|
61ee467dee | ||
|
|
7c0829d553 | ||
|
|
68eb4c6965 | ||
|
|
6d4015f61b | ||
|
|
d89513b7e0 | ||
|
|
a159594a45 | ||
|
|
78c62ad819 | ||
|
|
b3247d5a3b | ||
|
|
91f920e14c | ||
|
|
bcb7cde44b | ||
|
|
052c9458b7 | ||
|
|
3fe8c27d21 | ||
|
|
1dd256b210 | ||
|
|
dc9c6ae769 | ||
|
|
2cdafed2e0 | ||
|
|
ce90d5cd0a | ||
|
|
b4caa9137d | ||
|
|
dcca77c936 | ||
|
|
871b1baacc | ||
|
|
4deb699066 | ||
|
|
ccc8e053c6 | ||
|
|
6405056c92 | ||
|
|
9f9304fdc5 | ||
|
|
3d3b05c7a5 | ||
|
|
543a6b5836 | ||
|
|
b0de68919a | ||
|
|
d65ad5ea90 | ||
|
|
383fa6c0e0 | ||
|
|
8d40b3af70 | ||
|
|
9c80d7a4ec | ||
|
|
2360bde13e | ||
|
|
d25ae099d4 | ||
|
|
2d492886e4 | ||
|
|
33741436c7 | ||
|
|
318cc7fcaf | ||
|
|
bebcf0b196 | ||
|
|
f2be2e4eb1 | ||
|
|
a2ea017046 | ||
|
|
fc338c8958 | ||
|
|
bbb27f1224 | ||
|
|
d2fbdb6c48 | ||
|
|
38c70842d9 | ||
|
|
0e22c4c591 | ||
|
|
70e0eede16 | ||
|
|
4858a379e7 | ||
|
|
3e33755a9e | ||
|
|
f2ad004f33 | ||
|
|
16e0d9e355 | ||
|
|
d430b80191 | ||
|
|
2ffa8707ea | ||
|
|
cbd06cbd8e | ||
|
|
d3aa1acfbf | ||
|
|
c2232ae07a | ||
|
|
cf0a865d6f | ||
|
|
4942b3e6ad | ||
|
|
f374a1512a | ||
|
|
1558c848f4 | ||
|
|
4a2bb8fc43 | ||
|
|
fad21af146 | ||
|
|
850a628978 | ||
|
|
4d2311841d | ||
|
|
5c4175cd5f | ||
|
|
2931e40c7b | ||
|
|
6a6eae1ce1 | ||
|
|
210642e96c | ||
|
|
c97fad30ef | ||
|
|
268c9afec3 | ||
|
|
5c919ae565 | ||
|
|
e29d92c5ff | ||
|
|
0a331e3366 | ||
|
|
32d0e146ad | ||
|
|
c94f411fc6 | ||
|
|
22d408aa7b | ||
|
|
a44fdc1b11 | ||
|
|
50ca830ec6 | ||
|
|
e643cd3824 | ||
|
|
2660a50d31 | ||
|
|
927912efe3 | ||
|
|
9b49a24e74 | ||
|
|
0824b22532 | ||
|
|
000feead00 | ||
|
|
d582e53f73 | ||
|
|
52ad7d12cb | ||
|
|
10810308f0 | ||
|
|
4ce1058448 | ||
|
|
0db06df77f | ||
|
|
fdca53d4d2 | ||
|
|
8395598328 | ||
|
|
1b8635db32 | ||
|
|
60db1e02b0 | ||
|
|
a86e9c3609 | ||
|
|
6d77b22f97 | ||
|
|
e1b3756166 | ||
|
|
edf354f416 | ||
|
|
954a979a83 | ||
|
|
fe9a87fb06 | ||
|
|
1842a15f74 | ||
|
|
a568238472 | ||
|
|
fa200022c5 | ||
|
|
a6347341e3 | ||
|
|
c41481e488 | ||
|
|
767abc3c0c | ||
|
|
e005c24fbf | ||
|
|
8580f544fc | ||
|
|
2daa8ec944 | ||
|
|
2b92014ea5 | ||
|
|
f540679513 | ||
|
|
d165cc6e8e | ||
|
|
c1878fe54f | ||
|
|
f05948629e | ||
|
|
3e37dab4a1 | ||
|
|
6584e1f960 | ||
|
|
64c5a67a0e | ||
|
|
aea4e560ea | ||
|
|
12554a0004 | ||
|
|
83336ef9a5 | ||
|
|
08ab21b50e | ||
|
|
5628346ae1 | ||
|
|
b165127d20 | ||
|
|
905e2238d4 | ||
|
|
47bb1f657c | ||
|
|
c917d18d67 | ||
|
|
93568cd53f | ||
|
|
6af06559f4 | ||
|
|
43239a0485 | ||
|
|
e05f4097d6 | ||
|
|
35e62eaee9 | ||
|
|
858c2e66e6 | ||
|
|
0d156171d4 | ||
|
|
b57cb6c8e1 | ||
|
|
5ddff1bd37 | ||
|
|
036f923e05 | ||
|
|
130f4764a7 | ||
|
|
c4b5e5f8a2 | ||
|
|
ecb21df941 | ||
|
|
71d11ec103 | ||
|
|
213f0d3a93 | ||
|
|
8948eca0f3 | ||
|
|
aa0c841aff | ||
|
|
a8449382f0 | ||
|
|
5b27d9258e | ||
|
|
2590cf3895 | ||
|
|
a9f7068b2f | ||
|
|
1f9e4929a6 | ||
|
|
d56afca33e | ||
|
|
8fa023b4ec | ||
|
|
d9f73c4c6d | ||
|
|
68cf1c5410 | ||
|
|
a70220c6c4 | ||
|
|
99207d1d8f | ||
|
|
0baf2ecd70 | ||
|
|
2c6f830223 | ||
|
|
98eb1b596d | ||
|
|
145d222653 | ||
|
|
67e54572a8 | ||
|
|
3cc9b33a8d | ||
|
|
12bc94fbc0 | ||
|
|
b2379bfa5b | ||
|
|
f4fc13fc2f | ||
|
|
95081c43a7 | ||
|
|
bb3b3d5a47 | ||
|
|
f635f98ccb | ||
|
|
eef44295db | ||
|
|
5e1afc61c1 | ||
|
|
8edcbb3dc1 | ||
|
|
dca3722cb8 | ||
|
|
7eb0e93731 | ||
|
|
6034c3ff7a | ||
|
|
4eeb4a12a7 | ||
|
|
6daa689888 | ||
|
|
cded44b40a | ||
|
|
290c9f6b20 | ||
|
|
be3d712fdf | ||
|
|
f5666bcba1 | ||
|
|
f3c89bed01 | ||
|
|
9b0f170730 | ||
|
|
74350cd443 | ||
|
|
61aa7bb3b0 | ||
|
|
82fdbeda49 | ||
|
|
196b220faf | ||
|
|
f41c484913 | ||
|
|
a1a20b3f4b | ||
|
|
bbf9bee28e | ||
|
|
24a1ef9594 | ||
|
|
c5adef0a05 | ||
|
|
508c197446 | ||
|
|
473ec834fe | ||
|
|
66968904bf | ||
|
|
f169792e59 | ||
|
|
39a23af138 | ||
|
|
d8715819dc | ||
|
|
12444807e8 | ||
|
|
92077d47a1 | ||
|
|
4f54276e1b | ||
|
|
ac30848019 | ||
|
|
921650f53b | ||
|
|
72d81e8a85 | ||
|
|
5c5974106d | ||
|
|
c2cbd3a127 | ||
|
|
8e5aedec00 | ||
|
|
13965acb37 | ||
|
|
8a99b9d401 | ||
|
|
f739a1d476 | ||
|
|
87dd00be13 | ||
|
|
8cd393597a | ||
|
|
b7366ef672 | ||
|
|
3d6329dee8 | ||
|
|
2171420e0c | ||
|
|
e44a9b1ded | ||
|
|
bde5a88295 | ||
|
|
11babd5567 | ||
|
|
4c35d483bc | ||
|
|
d049558728 | ||
|
|
8eed42387c | ||
|
|
5b3366f35a | ||
|
|
fea7397c3b | ||
|
|
4a33582482 | ||
|
|
1bad0553b7 | ||
|
|
ac0b723bb0 | ||
|
|
39454a6c09 | ||
|
|
569157aded | ||
|
|
d2d7770c76 | ||
|
|
5da846f1ce | ||
|
|
713c9ec923 | ||
|
|
b3369616d2 | ||
|
|
082b8ef080 | ||
|
|
0fb9226a9b | ||
|
|
bb55f45d94 | ||
|
|
5b12980b7a | ||
|
|
493ef59dda | ||
|
|
b42d835cbf | ||
|
|
d1d177f80f | ||
|
|
433e5814ed | ||
|
|
2bf9f42b49 | ||
|
|
65efb573bd | ||
|
|
26a701215d | ||
|
|
3be7a20f40 | ||
|
|
6f8092f5a4 | ||
|
|
652fcfe799 | ||
|
|
d7d03ee6a8 | ||
|
|
dc6335ed4d | ||
|
|
b421f6bd7f | ||
|
|
d4358e0793 | ||
|
|
a8dfa0771f | ||
|
|
6df3ce4b19 | ||
|
|
8f40f8cab8 | ||
|
|
0d5a2cee58 | ||
|
|
8db4f41fa9 | ||
|
|
b85b03583b | ||
|
|
28e18fe87b | ||
|
|
2c1961acd7 | ||
|
|
04386f1a5e | ||
|
|
9c915e52ca | ||
|
|
6b6ab48377 | ||
|
|
bf36d07670 | ||
|
|
7166192143 | ||
|
|
509f0790db | ||
|
|
67a776a39a | ||
|
|
d3701df4e6 | ||
|
|
0195e34a7b | ||
|
|
0aa49c8a93 | ||
|
|
437de58922 | ||
|
|
fc76cf21fb | ||
|
|
23d1b25c46 | ||
|
|
8bfb59875f | ||
|
|
36e866743d | ||
|
|
d35c6e35ce | ||
|
|
e9afd8c99e | ||
|
|
43b7c41c4f | ||
|
|
921f60475e | ||
|
|
58433cda01 | ||
|
|
ed5eca5496 | ||
|
|
eff4a1f23e | ||
|
|
9f72b15fcf | ||
|
|
8069ff2b05 | ||
|
|
10deddf2f0 | ||
|
|
3ad1c4076d | ||
|
|
943f31e117 | ||
|
|
c43bc195e7 | ||
|
|
92b1e8f256 | ||
|
|
985970d320 | ||
|
|
5eabaa5207 | ||
|
|
7f60ecc725 | ||
|
|
7c635e1283 | ||
|
|
4727a7adf4 | ||
|
|
8940f14dde | ||
|
|
2fd0ce1a09 | ||
|
|
638bcc0f85 | ||
|
|
d411da3114 | ||
|
|
0acae46835 | ||
|
|
61cc61d9aa | ||
|
|
194320d163 | ||
|
|
902bfb8628 | ||
|
|
b25d72f698 | ||
|
|
a4d8f541dd | ||
|
|
6aaf2ff4d5 | ||
|
|
b0f77d6591 | ||
|
|
f2885dafd2 | ||
|
|
b0725e0153 | ||
|
|
4ce62034ce | ||
|
|
7d4c0ad611 | ||
|
|
d189cd9f23 | ||
|
|
f61ba70bb9 | ||
|
|
6e48692637 | ||
|
|
f7b1bc0e5c | ||
|
|
a213d6f7e1 | ||
|
|
a7d793ecf9 | ||
|
|
5d25481f66 | ||
|
|
77a8a783de | ||
|
|
0492eb7958 | ||
|
|
d37feea299 | ||
|
|
ffb9ad2094 | ||
|
|
1e63dfc4c5 | ||
|
|
051cc51d4b | ||
|
|
56dd92fee8 | ||
|
|
51b835b57f | ||
|
|
ccd7342986 | ||
|
|
82decaa070 | ||
|
|
d1e808d090 | ||
|
|
7aad389338 | ||
|
|
7e07c460de | ||
|
|
81a8ec3ce1 | ||
|
|
800f0a216d | ||
|
|
dade7dc069 | ||
|
|
b271d0a9a2 | ||
|
|
5daa0bc38e | ||
|
|
22665dc9b4 | ||
|
|
a99f11fb10 | ||
|
|
81824575e6 | ||
|
|
f6d010056a | ||
|
|
ffd20bcc6e | ||
|
|
55eaaa869d | ||
|
|
c2e66ca474 | ||
|
|
62b848c2e2 | ||
|
|
dbb657bd91 | ||
|
|
de871b79b0 | ||
|
|
146a6d459d | ||
|
|
9fb1dd18a7 | ||
|
|
9ae894d9e3 | ||
|
|
56e0fbdb05 | ||
|
|
e6325629a9 | ||
|
|
9a3329acc7 | ||
|
|
75ab225d84 | ||
|
|
883668ac93 | ||
|
|
c5f2dece49 | ||
|
|
b55bc5318e | ||
|
|
a39450c2b2 | ||
|
|
b784068701 | ||
|
|
bac4e68b89 | ||
|
|
20e95a4cab | ||
|
|
96da457db3 | ||
|
|
37b8715cff | ||
|
|
7aa3a243ba | ||
|
|
f2b549f9cd | ||
|
|
bc58dba0ad | ||
|
|
8ef9be5290 | ||
|
|
d95560db09 | ||
|
|
98f64df738 | ||
|
|
8634e372da | ||
|
|
cbda7633e0 | ||
|
|
8f00cf50ca | ||
|
|
acca28075b | ||
|
|
9a563463dd | ||
|
|
1ede94b09e | ||
|
|
7f41a54c4e | ||
|
|
6cfab79ce9 | ||
|
|
f33b3c7eef | ||
|
|
93d78ae2b0 | ||
|
|
c498b02289 | ||
|
|
30b62e6f18 | ||
|
|
ec18ef651b | ||
|
|
aad23686fa | ||
|
|
1c85ecd7c0 | ||
|
|
a1ff765235 | ||
|
|
a30ba9226d | ||
|
|
980a1e53a7 | ||
|
|
00092cd2a8 | ||
|
|
53ddb54936 | ||
|
|
ea1e307a51 | ||
|
|
17bc1e2e06 | ||
|
|
ed8f3b730b | ||
|
|
0e109d5237 | ||
|
|
f672226a3d | ||
|
|
481dccd085 | ||
|
|
9612f1c3c8 | ||
|
|
9e1e5f61a3 | ||
|
|
99a566f473 | ||
|
|
1012307467 | ||
|
|
b729f45eaf | ||
|
|
cab799e6ee | ||
|
|
a06322d155 | ||
|
|
d836cb2a2c | ||
|
|
2249c7a28a | ||
|
|
0d0422c0a7 | ||
|
|
5ea556ee77 | ||
|
|
a34496663b | ||
|
|
abeaa71d8d | ||
|
|
281a427718 | ||
|
|
03ab966cdc | ||
|
|
3dcb810346 | ||
|
|
d6eef8b3e1 | ||
|
|
a770f1cbf2 | ||
|
|
a5687df9b4 | ||
|
|
202881cbbd | ||
|
|
24ac3b9d8b | ||
|
|
47c1fb88b9 | ||
|
|
170b143b17 | ||
|
|
3854b49c35 | ||
|
|
bae4f604b5 | ||
|
|
36f32e14d3 | ||
|
|
9e521e6927 | ||
|
|
4176f541fc | ||
|
|
18f2afbf85 | ||
|
|
f9a94d9758 | ||
|
|
d1e672e3e3 | ||
|
|
202b924e63 | ||
|
|
4c3342aa99 | ||
|
|
b1e5dd1d1d | ||
|
|
d43ebfbdb9 | ||
|
|
cfadd8307b | ||
|
|
fa6cf9832d | ||
|
|
0075b0da33 | ||
|
|
59c6367cb5 | ||
|
|
194323a9ee | ||
|
|
1ca437e4ee | ||
|
|
77e43b9f50 | ||
|
|
a23f4b0224 | ||
|
|
0541c90980 | ||
|
|
b73a263de8 | ||
|
|
d7ffee68cf | ||
|
|
89cd42b904 | ||
|
|
404185eb8c | ||
|
|
8c29a81b12 | ||
|
|
9b42c6c379 | ||
|
|
03c2dd9e4b | ||
|
|
d8274c3d8c | ||
|
|
85b5fb090f | ||
|
|
2adef311ed | ||
|
|
7050d0b8ad | ||
|
|
8358144a4f | ||
|
|
44e083e0a9 | ||
|
|
a5418cf6ee | ||
|
|
c74630d6eb | ||
|
|
157d4ac9a9 | ||
|
|
8228613c01 | ||
|
|
b907e17b70 | ||
|
|
aa7000427c | ||
|
|
8562c90454 | ||
|
|
5a9a04cd72 | ||
|
|
6b32611356 | ||
|
|
770ebc394f | ||
|
|
c0d35e84b1 | ||
|
|
5e74478864 | ||
|
|
46852e2051 | ||
|
|
b9dded0bef | ||
|
|
6d05f403a5 | ||
|
|
8368634f85 | ||
|
|
8d57d909b4 | ||
|
|
9b7855d597 | ||
|
|
6a5e1b9998 | ||
|
|
af6e558699 | ||
|
|
26d4dddffd | ||
|
|
e2765b4881 | ||
|
|
9796b25f33 | ||
|
|
01b8d3a314 | ||
|
|
50c48de0de | ||
|
|
bfc90da054 | ||
|
|
9d8d6fd6b1 | ||
|
|
43344d9a78 | ||
|
|
01a593c857 | ||
|
|
1b2dd11a4c | ||
|
|
d90a2c4801 | ||
|
|
f065c21ee6 | ||
|
|
d0324bd497 | ||
|
|
e57fba07db | ||
|
|
5cb279cf4e | ||
|
|
4f728b0387 | ||
|
|
927ae08a47 | ||
|
|
a28dc47e30 | ||
|
|
b43ee13dd8 | ||
|
|
ad5ad5a675 | ||
|
|
198f7d171e | ||
|
|
549425cb81 | ||
|
|
6023165268 | ||
|
|
2a7273c254 | ||
|
|
8640934410 | ||
|
|
20bc4e7caa | ||
|
|
9f83058b78 | ||
|
|
ffedf29db4 | ||
|
|
00219d342a | ||
|
|
753400144d | ||
|
|
854a732f47 | ||
|
|
03eaf07526 | ||
|
|
d04c12d8cb | ||
|
|
afddd3543e | ||
|
|
d9459a855d | ||
|
|
efbcfca126 | ||
|
|
916675a9bd | ||
|
|
7b43617954 | ||
|
|
09b89bccfd | ||
|
|
1190ee07a9 | ||
|
|
c4dcf405bb | ||
|
|
c844ea6f07 | ||
|
|
19704c151a | ||
|
|
1281ba18cd | ||
|
|
f1f2fae420 | ||
|
|
5809070b05 | ||
|
|
2c492540ce | ||
|
|
61556b505d | ||
|
|
d6a4bc87e8 | ||
|
|
e49f4696e6 | ||
|
|
4219f50141 | ||
|
|
19da003291 | ||
|
|
f0765e3d6a | ||
|
|
8dac345512 | ||
|
|
ce101843f0 | ||
|
|
d56dc3a237 | ||
|
|
28cffb2e25 | ||
|
|
d2d169fea3 | ||
|
|
a5eb1b15d2 | ||
|
|
23dbb0b41c | ||
|
|
c72ae68de5 | ||
|
|
7c1f87aed3 | ||
|
|
6ec0ac1651 | ||
|
|
9dc84e5c2b | ||
|
|
1e17b55f49 | ||
|
|
6a4c8556a3 | ||
|
|
488f1c8b83 | ||
|
|
e2537d8e93 | ||
|
|
6d35cb6c77 | ||
|
|
8ce7f64679 | ||
|
|
5cd6250f05 | ||
|
|
4872cd91a5 | ||
|
|
08e84e2ca0 | ||
|
|
40889c9637 | ||
|
|
010ba6f214 | ||
|
|
2f8dc860ff | ||
|
|
88f61f0d45 | ||
|
|
16a5aa7489 | ||
|
|
89fd2b2ed3 | ||
|
|
a16769061e | ||
|
|
3be76145b7 | ||
|
|
e9d27fa632 | ||
|
|
be706e4bb7 | ||
|
|
b87c4d5623 | ||
|
|
03a260f26a | ||
|
|
ae3e007a5f | ||
|
|
cfbf76d898 | ||
|
|
0b82b121cb | ||
|
|
643ebb4946 | ||
|
|
8693eed3ec | ||
|
|
766b910150 | ||
|
|
545843ffe6 | ||
|
|
39e7b23d5b | ||
|
|
9d0db6f74c | ||
|
|
78fb25329c | ||
|
|
7db6a7b57a | ||
|
|
76f70179c0 | ||
|
|
d6aecfe9ae | ||
|
|
1354d3dec1 | ||
|
|
955ae817d8 | ||
|
|
aee9589c1a | ||
|
|
48ac0f55c3 | ||
|
|
831c0a340c | ||
|
|
210d297d18 | ||
|
|
afbcf9039e | ||
|
|
e8eadc1a09 | ||
|
|
f234b7015c | ||
|
|
8de38b3415 | ||
|
|
0f132fc180 | ||
|
|
e597c24f35 | ||
|
|
1d7a913d29 | ||
|
|
f89131205d | ||
|
|
abc6a98d0f | ||
|
|
9415e79ff3 | ||
|
|
6269022536 | ||
|
|
6ad2125ee7 | ||
|
|
f5d7523a4f | ||
|
|
ce31b9af7e | ||
|
|
fc5d980a52 | ||
|
|
9ea726732a | ||
|
|
db5f9cca73 | ||
|
|
06fb4f6972 | ||
|
|
f045f9cf1b | ||
|
|
1752f57c7d | ||
|
|
7eb4bc15b8 | ||
|
|
707b5ea851 | ||
|
|
fe9f679051 | ||
|
|
368a49be36 | ||
|
|
80f0c5f5d7 | ||
|
|
a5a7224228 | ||
|
|
d616cf045b | ||
|
|
8186ae060d | ||
|
|
c46e80c711 | ||
|
|
0735ebd7a8 | ||
|
|
391bbaa73b | ||
|
|
fed96c0e45 | ||
|
|
f97f89a371 | ||
|
|
6352a5322b | ||
|
|
3473427484 | ||
|
|
d6c1733f0c | ||
|
|
d641ac197b | ||
|
|
6a0e41b05e | ||
|
|
e21a9a095e | ||
|
|
56e1a0b939 | ||
|
|
f9f9127a64 | ||
|
|
e0628c4ae3 | ||
|
|
43be342dea | ||
|
|
9cc7419700 | ||
|
|
f559caf4d0 | ||
|
|
42af54ca04 | ||
|
|
9f55cd46d8 | ||
|
|
5956f6b123 | ||
|
|
41e42b8a53 | ||
|
|
f6f5fbd6fb | ||
|
|
c7618e3991 | ||
|
|
da6df5da9e | ||
|
|
480cc79de4 | ||
|
|
231c13d9a5 | ||
|
|
000406ec0b | ||
|
|
44e0a8bcb2 | ||
|
|
d2e88c7a2f | ||
|
|
638cddcd5e | ||
|
|
a3924f4ab1 | ||
|
|
5798c396ec | ||
|
|
b1983f0a83 | ||
|
|
aca9f562e1 | ||
|
|
8c6a830521 | ||
|
|
9142cf19c0 | ||
|
|
887b7fb34a | ||
|
|
1392547783 | ||
|
|
735b9b8c7a | ||
|
|
cba974ec34 | ||
|
|
421d588a2f | ||
|
|
8b848277d2 | ||
|
|
8ae19c7243 | ||
|
|
1e5051ef79 | ||
|
|
d293dbc10f | ||
|
|
703f481a9a | ||
|
|
68f0c12cab | ||
|
|
8c7598f45d | ||
|
|
192c4f239e | ||
|
|
64e09315fc | ||
|
|
db0c4155b8 | ||
|
|
f971bfd80a | ||
|
|
948244b45c | ||
|
|
c7fecbed7a | ||
|
|
f22cae98ef | ||
|
|
4ba02c97a7 | ||
|
|
0581dc7763 | ||
|
|
77261d4872 | ||
|
|
cfd14ef169 | ||
|
|
c7cc76b044 | ||
|
|
44e1096753 | ||
|
|
dc1c48ad4e | ||
|
|
d9d29220cc | ||
|
|
6f744ef311 | ||
|
|
f7071f2231 | ||
|
|
ef75c9a35f | ||
|
|
4ca3c6c96a | ||
|
|
82e98410ed | ||
|
|
dcd92f03db | ||
|
|
ba6c747e55 | ||
|
|
b53b766ee5 | ||
|
|
cd737fccb5 | ||
|
|
7fb3acea96 | ||
|
|
867052f974 | ||
|
|
038e010819 | ||
|
|
7f5e42fdd5 | ||
|
|
2fe9b3dcbe | ||
|
|
89dbfcecbf | ||
|
|
f4bda6b91f | ||
|
|
dba72ac8a7 | ||
|
|
aedbfb11af | ||
|
|
43fd380e25 | ||
|
|
2872100ff0 | ||
|
|
afe81048c9 | ||
|
|
9d44857d77 | ||
|
|
4a7d186f27 | ||
|
|
5b1fd12edd | ||
|
|
8309a2aed9 | ||
|
|
e15d6a2239 | ||
|
|
abf04ac96f | ||
|
|
8ec6275f9a | ||
|
|
3e2bfb20f5 | ||
|
|
ee13087d57 | ||
|
|
f2fd7de9db | ||
|
|
dcfd7e05ce | ||
|
|
e47b69c33a | ||
|
|
383f78c8ca | ||
|
|
54042d5150 | ||
|
|
90e6ffc928 | ||
|
|
be65f49bbd | ||
|
|
d81b8ff98f | ||
|
|
9b5201e33c | ||
|
|
588c1b91b9 | ||
|
|
eb63734792 | ||
|
|
22cc585180 | ||
|
|
bd1631a649 | ||
|
|
e12bde6cdb | ||
|
|
7763ceefc0 | ||
|
|
05a7d998b9 | ||
|
|
4b676ac327 | ||
|
|
6ad1322734 | ||
|
|
03248735ac | ||
|
|
8e5651490c | ||
|
|
0d7aa2a101 | ||
|
|
83669ced3d | ||
|
|
2a33d23165 | ||
|
|
b7ae915784 | ||
|
|
9fcfa25460 | ||
|
|
946eae00bd | ||
|
|
e8ef33c9a1 | ||
|
|
8c8654f2d8 | ||
|
|
4b744791f2 | ||
|
|
5327cb33f9 | ||
|
|
d9ddc6fd90 | ||
|
|
a94c2cfa1e | ||
|
|
614bc6b480 | ||
|
|
47585174d8 | ||
|
|
5dd11bcc9b | ||
|
|
160d2123b2 | ||
|
|
ca55ed3a21 | ||
|
|
34fe820a26 | ||
|
|
c7a4d23662 | ||
|
|
432a7120f2 | ||
|
|
7e8b5d28f7 | ||
|
|
079871df38 | ||
|
|
c95d3ef07d | ||
|
|
acad455ccb | ||
|
|
fcc7df32ad | ||
|
|
c7f0484c37 | ||
|
|
3dac31a771 | ||
|
|
16357963d5 | ||
|
|
7d7492b9aa | ||
|
|
26dff7e00e | ||
|
|
cf410e310f | ||
|
|
5d4855f86c | ||
|
|
6d7e2b8a06 | ||
|
|
d37be0d059 | ||
|
|
d419ec5776 | ||
|
|
c01f100858 | ||
|
|
d8e380b53f | ||
|
|
6881c39c8d | ||
|
|
e7fa540403 | ||
|
|
e92ba27893 | ||
|
|
210792397d | ||
|
|
91385967c1 | ||
|
|
0c8d27964b | ||
|
|
ed4fcb71f1 | ||
|
|
0738f6a2d6 | ||
|
|
1e2a874067 | ||
|
|
9d2f8cbd8a | ||
|
|
dd1c653365 | ||
|
|
1a0a53a26f | ||
|
|
97ea4e2a26 | ||
|
|
b408d9348d | ||
|
|
44cfde7a0c | ||
|
|
db3d7ee436 | ||
|
|
a05f713e26 | ||
|
|
dfb492493c | ||
|
|
7ddf4169b8 | ||
|
|
89d6a3d91a | ||
|
|
16196eeaaa | ||
|
|
393d4163c8 | ||
|
|
5855aa2689 | ||
|
|
4e59f55f11 | ||
|
|
b269487a47 | ||
|
|
dce4028786 | ||
|
|
819ad4c770 | ||
|
|
b10b6d4833 | ||
|
|
a3d224beaf | ||
|
|
164ad8c521 | ||
|
|
4c8ec8befe | ||
|
|
0692334a27 | ||
|
|
be912cf2ce | ||
|
|
f942c088ec | ||
|
|
330da2dcbb | ||
|
|
afad0395ff | ||
|
|
ecd72f1bc0 | ||
|
|
867daeead7 | ||
|
|
a20c3a397e | ||
|
|
a1a22e811f | ||
|
|
3fbbabd68a | ||
|
|
47c1202535 | ||
|
|
83a5feaaa8 | ||
|
|
6e767fc71d | ||
|
|
303f264326 | ||
|
|
f73671845c | ||
|
|
82c04caf3a | ||
|
|
d8080d1336 | ||
|
|
fc09d1c09a | ||
|
|
65a47c01ec | ||
|
|
e9f2e0d7d7 | ||
|
|
ddcb31897d | ||
|
|
eecdc0e24c | ||
|
|
f642bcc954 | ||
|
|
7dedac5880 | ||
|
|
ee5362f81a | ||
|
|
1475137d4d | ||
|
|
8ba028ebbb | ||
|
|
7cd385e8c2 | ||
|
|
41694b3666 | ||
|
|
25314f370e | ||
|
|
664b88c1bc | ||
|
|
d6adffa273 | ||
|
|
5bd039a335 | ||
|
|
0782cc3c21 | ||
|
|
c73628cca1 | ||
|
|
8ffc5aa20c | ||
|
|
89c2f09864 | ||
|
|
ee3115653c | ||
|
|
48115fc150 | ||
|
|
d804a363ae | ||
|
|
867c084990 | ||
|
|
8ffbfca97b | ||
|
|
ca9a72d25c | ||
|
|
acdbe8ef5e | ||
|
|
5cc3a5b71c | ||
|
|
bd587fd21b | ||
|
|
0f580ff23d | ||
|
|
b0b105babc | ||
|
|
3b0cc5f3a8 | ||
|
|
e50981ccc0 | ||
|
|
24f3c20f26 | ||
|
|
ca5d71a491 | ||
|
|
a022a2b4a4 | ||
|
|
0cb618b9b1 | ||
|
|
1f4927683e | ||
|
|
7c74399a5d | ||
|
|
52903b6ecd | ||
|
|
a43244916b | ||
|
|
35be892e69 | ||
|
|
536e8ffb28 | ||
|
|
0241f07105 | ||
|
|
3570af90ab | ||
|
|
fa3fa9e2ef | ||
|
|
83e2bb028f | ||
|
|
f50b7c4301 | ||
|
|
41c9a0c395 | ||
|
|
12401b6588 | ||
|
|
8edf8905a5 | ||
|
|
d5cb8d187c | ||
|
|
328a066f0f | ||
|
|
42d314d592 | ||
|
|
d89d546e53 | ||
|
|
1a558143c7 | ||
|
|
ad7ffa2245 | ||
|
|
717ac420d9 | ||
|
|
abff323eb6 | ||
|
|
03bc1ccd4a | ||
|
|
da408eeaff | ||
|
|
a2ba9bb26a | ||
|
|
16c41ed046 | ||
|
|
aeb3c9f094 | ||
|
|
2562cc8220 | ||
|
|
0647cd85e9 | ||
|
|
d57e7389d2 | ||
|
|
3cb3316a95 | ||
|
|
8ba03e1205 | ||
|
|
6a69d3c07b | ||
|
|
c054b8c9ab | ||
|
|
db6f7cceea | ||
|
|
0898ee1fba | ||
|
|
6366c0a684 | ||
|
|
f56eb42c90 | ||
|
|
6f3f21921e | ||
|
|
b8c016c3e9 | ||
|
|
e5d3b1c1d5 | ||
|
|
fe140b6319 | ||
|
|
4e1e0aad8a | ||
|
|
84a5515a3d | ||
|
|
0d5073f8ff | ||
|
|
05a4a101db | ||
|
|
848f18be2b | ||
|
|
21c9079087 | ||
|
|
2935192eec | ||
|
|
f896db033f | ||
|
|
b197afe1a0 | ||
|
|
dd07f7f580 | ||
|
|
8376f9093b | ||
|
|
38b8ed7bf9 | ||
|
|
aa74ea0d0a | ||
|
|
e5d3f62043 | ||
|
|
d2d0e70678 | ||
|
|
aceb7d0467 | ||
|
|
c7ac1fa630 | ||
|
|
5855a5b26c | ||
|
|
0301af62b8 | ||
|
|
32e9757bf7 | ||
|
|
d2b883c4b5 | ||
|
|
816f40a2d5 | ||
|
|
7e7f38005a | ||
|
|
fb834f7207 | ||
|
|
5a3e5296d0 | ||
|
|
a0e8d8f2d8 | ||
|
|
9c6dfff531 | ||
|
|
84d9580bae | ||
|
|
8d6de642ea | ||
|
|
b20b812cfe | ||
|
|
9b23ff6a19 | ||
|
|
cc5278a117 | ||
|
|
ca6459afe4 | ||
|
|
f8477f007d | ||
|
|
82d1ad278c | ||
|
|
761fb608d3 | ||
|
|
af328eee90 | ||
|
|
0fde11ae72 | ||
|
|
73919b1943 | ||
|
|
1433439215 | ||
|
|
4c8dc09f67 | ||
|
|
80b43942b0 | ||
|
|
b709b53b6a | ||
|
|
f4e3e5b618 | ||
|
|
bac7299359 | ||
|
|
fc6f535edd | ||
|
|
7e2ffab62c | ||
|
|
214d721380 | ||
|
|
2f8227e652 | ||
|
|
c0c02d290f | ||
|
|
bc2aa71888 | ||
|
|
7f187d1553 | ||
|
|
81b1f4fdc1 | ||
|
|
15f088f10a | ||
|
|
286f12522b | ||
|
|
9d8a6d0142 | ||
|
|
a2dd97cc74 | ||
|
|
1d9a6ea3c0 | ||
|
|
3ca8b1017b | ||
|
|
ecd7c76aee | ||
|
|
70edcd62b9 | ||
|
|
90f531888c | ||
|
|
953de733e7 | ||
|
|
e0ed443319 | ||
|
|
1487720fd8 | ||
|
|
828567e0ce | ||
|
|
78da4c7fce | ||
|
|
7f80bfd2cb | ||
|
|
7e3deb5e3f | ||
|
|
5475ec4f0c | ||
|
|
58b4585b57 | ||
|
|
cf9ab1c47b | ||
|
|
65233cdd5c | ||
|
|
9d13cb644d | ||
|
|
dd9e0343e8 | ||
|
|
bb4aaa2a78 | ||
|
|
0023418b94 | ||
|
|
ec2c9ecea0 | ||
|
|
59759398a6 | ||
|
|
c4852cb451 | ||
|
|
99880d6e8a | ||
|
|
b005799d92 | ||
|
|
72f86dac27 | ||
|
|
83628facb3 | ||
|
|
f6e171823a | ||
|
|
9b743a319f | ||
|
|
eedf2a6cc8 | ||
|
|
e1e738f772 | ||
|
|
182ff3df88 | ||
|
|
23fde95f9e | ||
|
|
78f9132eb3 | ||
|
|
84b7211588 | ||
|
|
2793e15c26 | ||
|
|
36bd76607a | ||
|
|
e17f416bb0 | ||
|
|
a577b1e22e | ||
|
|
2d324c77c1 | ||
|
|
88374eca74 | ||
|
|
386135788b | ||
|
|
a943d955dd | ||
|
|
15476ac32e | ||
|
|
17fb5b9eae | ||
|
|
fd27a0dfa2 | ||
|
|
5ffe69ec6c | ||
|
|
f5723b2a4d | ||
|
|
9e959a88f1 | ||
|
|
09abac41c5 | ||
|
|
1dbf013c1b | ||
|
|
a637d5b126 | ||
|
|
d409183751 | ||
|
|
e8feef1ce0 | ||
|
|
01491cc696 | ||
|
|
568a3f28cf | ||
|
|
02219df480 | ||
|
|
e006ca51ee | ||
|
|
86f651f3ec | ||
|
|
212b33a0ce | ||
|
|
6b7a121161 | ||
|
|
c89da718b1 | ||
|
|
eef3ae8387 | ||
|
|
0975a61725 | ||
|
|
0c53d54347 | ||
|
|
cbbb04cf69 | ||
|
|
120baf0a6e | ||
|
|
685dc7cc3a | ||
|
|
2fbb2ac77b | ||
|
|
2832db7aba | ||
|
|
18f2471d7c | ||
|
|
ea28f5a61b | ||
|
|
fe9cc6038e | ||
|
|
5ed335c499 | ||
|
|
fd04b97496 | ||
|
|
4c9d281b59 | ||
|
|
a26d66b424 | ||
|
|
cfb683419f | ||
|
|
97887bdf02 | ||
|
|
38ea693e73 | ||
|
|
582a0ea6a5 | ||
|
|
71b9f0907e | ||
|
|
82b82b714d | ||
|
|
6356228053 | ||
|
|
18fd677550 | ||
|
|
d9698df721 | ||
|
|
63839fe045 | ||
|
|
2ee06a7cbf | ||
|
|
cf2ba90b1d | ||
|
|
8124b2143b | ||
|
|
5361573051 | ||
|
|
1d377c868d | ||
|
|
b0a855a326 | ||
|
|
fa35f29596 | ||
|
|
f429d23b6e | ||
|
|
eeeb903b70 | ||
|
|
78f12c4a75 | ||
|
|
c69f34d1e2 | ||
|
|
ccfcc5d8b4 | ||
|
|
210c248264 | ||
|
|
e3258657d0 | ||
|
|
dbc518bf53 | ||
|
|
cdbdb4510e | ||
|
|
e7ec5e5ba4 | ||
|
|
03d8a7f296 | ||
|
|
480035c065 | ||
|
|
b32c18cf72 | ||
|
|
337a6b276a | ||
|
|
06cf1f9477 | ||
|
|
190cfd2b7a | ||
|
|
63035b4d66 | ||
|
|
6a11c8fcb1 | ||
|
|
33ffb006e3 | ||
|
|
162e7f63a9 | ||
|
|
bb581eeec4 | ||
|
|
272c555bc5 | ||
|
|
fb64caab23 | ||
|
|
6f2a74a46d | ||
|
|
ec65a74492 | ||
|
|
ba791c42fa | ||
|
|
5896667c36 | ||
|
|
003c949d38 | ||
|
|
d31394254c | ||
|
|
1a497e246c | ||
|
|
d24276aa54 | ||
|
|
6ed21a3e6b | ||
|
|
8066f9ce96 | ||
|
|
a0276ec1ce | ||
|
|
2ab925a24c | ||
|
|
78fbad7d8d | ||
|
|
6652ae3042 | ||
|
|
aa12ab7d76 | ||
|
|
89be4bdce9 | ||
|
|
d09a064471 | ||
|
|
2b18ca5a6c | ||
|
|
6bc2f18bbf | ||
|
|
fbb892d84e | ||
|
|
4efb7c20e4 | ||
|
|
4beb1f07a6 | ||
|
|
45e640e5f9 | ||
|
|
e84beffee3 | ||
|
|
e07c7483a7 | ||
|
|
34ec7bf5eb | ||
|
|
01b20a4829 | ||
|
|
45ea898da4 | ||
|
|
fd34c5070f | ||
|
|
6c409d96c1 | ||
|
|
0cbe4bb3d4 | ||
|
|
d04a990235 | ||
|
|
4747c87632 | ||
|
|
f57452c7bf | ||
|
|
9a8e2eb675 | ||
|
|
8ef7f3cbe2 | ||
|
|
de30a65f01 | ||
|
|
0bcf613195 | ||
|
|
89fd9004d0 | ||
|
|
b2be108db1 | ||
|
|
6102c44b70 | ||
|
|
327cdc8e41 | ||
|
|
0bc3613989 | ||
|
|
30cea871f9 | ||
|
|
5f332fe4db | ||
|
|
7ee7f2716b | ||
|
|
5e8c39cb1e | ||
|
|
ee355200b3 | ||
|
|
986fbe2254 | ||
|
|
32f93a2be1 | ||
|
|
ab79065c13 | ||
|
|
77171b7894 | ||
|
|
cce8ddb84f | ||
|
|
648b7e5911 | ||
|
|
67a66fdc93 | ||
|
|
5fbaa26d05 | ||
|
|
1f4a3c4fd2 | ||
|
|
959dc7ffd4 | ||
|
|
0e34221cac | ||
|
|
0cad13b3a3 | ||
|
|
0776f71ca3 | ||
|
|
5a74e369ce | ||
|
|
799c5ac662 | ||
|
|
1080cf0ef4 | ||
|
|
867d49e3fb | ||
|
|
c7be406099 | ||
|
|
37257e77ce | ||
|
|
270d9ff0fc | ||
|
|
34b48598d9 | ||
|
|
5105ecb148 | ||
|
|
f47b151458 | ||
|
|
d3f15a58fc | ||
|
|
42f6adb7fa | ||
|
|
263f8a8e7d | ||
|
|
3a42d30cfd | ||
|
|
9275217a5a | ||
|
|
1fed19ad68 | ||
|
|
6a8a78abd1 | ||
|
|
de69f51084 | ||
|
|
c81a3f39fd | ||
|
|
6fcb925e34 | ||
|
|
8823b7280a | ||
|
|
ebadad97a8 | ||
|
|
a095c39626 | ||
|
|
fb9bcb44c3 | ||
|
|
38f593450a | ||
|
|
6d44f2c5cb | ||
|
|
d1786a64c4 | ||
|
|
616ae99c0b | ||
|
|
4d2ff80788 | ||
|
|
005295fd4c | ||
|
|
d6b46f8243 | ||
|
|
bac35e8f1b | ||
|
|
a003c4da85 | ||
|
|
0ae8e4fe8a | ||
|
|
d3667dfc77 | ||
|
|
54c5467dc6 | ||
|
|
d01f0300a5 | ||
|
|
bff207008f | ||
|
|
ed739f95ff | ||
|
|
bb4de1336c | ||
|
|
f7205558d2 | ||
|
|
1318544ecf | ||
|
|
a81c161434 | ||
|
|
73acdc755f | ||
|
|
dd84ea8fda | ||
|
|
a2a4e40821 | ||
|
|
509f54d68c | ||
|
|
27ce460ea4 | ||
|
|
116794ad77 | ||
|
|
89feba870d | ||
|
|
2a980601c0 | ||
|
|
e1fd8ae910 | ||
|
|
9cb415008c | ||
|
|
26ba9bf94a | ||
|
|
fb091ca195 | ||
|
|
99a7dca3ce | ||
|
|
a1a03d94be | ||
|
|
85a2d41a72 | ||
|
|
89bd7e9011 | ||
|
|
ececdf42a7 | ||
|
|
2cf03de8b8 | ||
|
|
c8daa1730b | ||
|
|
d195b595dd | ||
|
|
ff3f7adff2 | ||
|
|
37781a9df7 | ||
|
|
ca1c60e00e | ||
|
|
649f382291 | ||
|
|
103a35c81b | ||
|
|
5af341bfdb | ||
|
|
7c7110cd83 | ||
|
|
c4ea489bff | ||
|
|
60b3b6ff03 | ||
|
|
1510dca065 | ||
|
|
2c49375928 | ||
|
|
f9fc22c48b | ||
|
|
8d14e7ab04 | ||
|
|
8f2a28a1f2 | ||
|
|
e8fb1faec0 | ||
|
|
0e4874c412 | ||
|
|
933630af28 | ||
|
|
ff87098102 | ||
|
|
6513a86bbd | ||
|
|
97e296c5d5 | ||
|
|
9f87adf4e8 | ||
|
|
6470b3f08c | ||
|
|
d1ba84525e | ||
|
|
05ad66c464 | ||
|
|
c41d38bf61 | ||
|
|
b155e8480b | ||
|
|
e94f687573 | ||
|
|
5883082d01 | ||
|
|
75b53ffda1 | ||
|
|
a8057b96f3 | ||
|
|
a89e809498 | ||
|
|
02f7a4fdc9 | ||
|
|
7c7c22a597 | ||
|
|
860fa525a2 | ||
|
|
9f0e38a28a | ||
|
|
f834b7befb | ||
|
|
7f3a45fb5b | ||
|
|
b0953e810b | ||
|
|
52a86012e5 | ||
|
|
2a06fe6baf | ||
|
|
6e688d2175 | ||
|
|
b610fd1f56 | ||
|
|
a4730096f4 | ||
|
|
6a02439944 | ||
|
|
b889b0d2b0 | ||
|
|
ba5733e4a4 | ||
|
|
2e0221ecfd | ||
|
|
aeded79544 | ||
|
|
c0a7958e77 | ||
|
|
431b194ec2 | ||
|
|
29d24d7e08 | ||
|
|
3b04c642ae | ||
|
|
609fc5b0c0 | ||
|
|
2a8c79b577 | ||
|
|
043316e40f | ||
|
|
c9d8b987d4 | ||
|
|
33c5a5c665 | ||
|
|
29c90ba163 | ||
|
|
8c19450b10 | ||
|
|
89c97458c4 | ||
|
|
02be516902 | ||
|
|
47ada300ea | ||
|
|
6f9c621774 | ||
|
|
324d9195c3 | ||
|
|
0310fe3b68 | ||
|
|
7144a6e4b7 | ||
|
|
1c8071a96f | ||
|
|
4ad115e024 | ||
|
|
f11d754cfe | ||
|
|
65019ce96f | ||
|
|
90e8fb2a57 | ||
|
|
ff5377da9e | ||
|
|
7aee84eb05 | ||
|
|
c0a7a6d0d6 | ||
|
|
29c48471f2 | ||
|
|
229e56464b | ||
|
|
769c32fd38 | ||
|
|
d05b89cfb3 | ||
|
|
f1f18a2334 | ||
|
|
afc556f801 | ||
|
|
7f240839fc | ||
|
|
8d0a6274cb | ||
|
|
91cab10ff8 | ||
|
|
5828ea7426 | ||
|
|
80ba147168 | ||
|
|
f85479ba11 | ||
|
|
a23c5f151f | ||
|
|
cab3601474 | ||
|
|
cf330fcd58 | ||
|
|
eb0162dccf | ||
|
|
a415aed7e6 | ||
|
|
9f6014dc0b | ||
|
|
21b0d38b57 | ||
|
|
1ec7f22b5f | ||
|
|
346c5e4a4c | ||
|
|
c9a0f0a713 | ||
|
|
8bbc38dc8a | ||
|
|
96494a22f9 | ||
|
|
0813789e2a | ||
|
|
98ca820ab1 | ||
|
|
81ca9d9f29 | ||
|
|
16fe0019f9 | ||
|
|
5fa650ab75 | ||
|
|
564c2a0f16 | ||
|
|
1685e1c310 | ||
|
|
332f5a1266 | ||
|
|
99ac85f06a | ||
|
|
fc30ad0935 | ||
|
|
aadf2eb609 | ||
|
|
68f778e423 | ||
|
|
0066cff652 | ||
|
|
f872a0c80a | ||
|
|
68d67a8194 | ||
|
|
756ac1514c | ||
|
|
d9478142b1 | ||
|
|
5eced59961 | ||
|
|
3e1862cd51 | ||
|
|
7271609850 | ||
|
|
f24337bb3b | ||
|
|
6167247ea2 | ||
|
|
0332e52501 | ||
|
|
0c23f78ab7 | ||
|
|
7624449644 | ||
|
|
97309e8c4c | ||
|
|
a1e78214db | ||
|
|
1a57b3296b | ||
|
|
7c10fc3514 | ||
|
|
494ef42aa8 | ||
|
|
8a54d216c6 | ||
|
|
896a20d76d | ||
|
|
a26510b02f | ||
|
|
1465f1d237 | ||
|
|
21b18d107a | ||
|
|
cc2173c587 | ||
|
|
71fe821ae9 | ||
|
|
3ffab69ad6 | ||
|
|
58a662bd35 | ||
|
|
eb55c256c7 | ||
|
|
b07e553b9e | ||
|
|
2d61709de3 | ||
|
|
1352e1d92d | ||
|
|
b595c3b902 | ||
|
|
76efb01c4c | ||
|
|
0078945b79 | ||
|
|
70d8ef0b8e | ||
|
|
9a1f7cba68 | ||
|
|
39e14acffe | ||
|
|
a6149390a0 | ||
|
|
33cdc75810 | ||
|
|
13f229089d | ||
|
|
2dbb0fca85 | ||
|
|
c7a8a4e30f | ||
|
|
dc6f1a0c10 | ||
|
|
178872d651 | ||
|
|
e3c15493a2 | ||
|
|
3f22e5da2d | ||
|
|
39ee4513c6 | ||
|
|
598898f0a1 | ||
|
|
052e783638 | ||
|
|
d2deda238a | ||
|
|
7317a8304f | ||
|
|
5ea9d70fb4 | ||
|
|
a73e0998bc | ||
|
|
51befe3e27 | ||
|
|
37569a291b | ||
|
|
7fe973145d | ||
|
|
da3fc276fd | ||
|
|
4c0af8b1c0 | ||
|
|
f65f7b2f1b | ||
|
|
33c86ed6a7 | ||
|
|
545b6c1247 | ||
|
|
1b0e37f371 | ||
|
|
97e00721e9 | ||
|
|
3c43301edb | ||
|
|
51cbb67a96 | ||
|
|
2e2bd80416 | ||
|
|
86389ff7a3 | ||
|
|
c2dfea18c4 | ||
|
|
4be6bbc681 | ||
|
|
cfc0ff9cc7 | ||
|
|
b4c6ba6962 | ||
|
|
dc3ed2f79b | ||
|
|
5d95c7a9cb | ||
|
|
6821ca6238 | ||
|
|
167dc0a259 | ||
|
|
fa9fa68693 | ||
|
|
353615da89 | ||
|
|
51de558675 | ||
|
|
174fc1dda1 | ||
|
|
7caeb60c47 | ||
|
|
e3aba1b5ff | ||
|
|
1a8b36f06d | ||
|
|
250303790c | ||
|
|
e20544659a | ||
|
|
ad7692ed34 | ||
|
|
f9144a4be3 | ||
|
|
0fc3b90efb | ||
|
|
25ccc8a77a | ||
|
|
da18390f3e | ||
|
|
b392bf8298 | ||
|
|
73c42ebeaf | ||
|
|
eb45f7fcba | ||
|
|
5fdca29448 | ||
|
|
048c4006e4 | ||
|
|
6d10a2dafb | ||
|
|
d6c421f8de | ||
|
|
ac4adcc62c | ||
|
|
bc8465d284 | ||
|
|
7fc497eeb8 | ||
|
|
633817e3d5 | ||
|
|
09dee9670a | ||
|
|
631debbec4 | ||
|
|
4e57bc2161 | ||
|
|
a111e36867 | ||
|
|
152bd5841c | ||
|
|
ecefdcd951 | ||
|
|
a976cfa98d | ||
|
|
df70d7708d | ||
|
|
c558d95e3b | ||
|
|
3bd4825d4b | ||
|
|
67cdcedd4e | ||
|
|
384116a76f | ||
|
|
12e91ae643 | ||
|
|
7c56d277da | ||
|
|
579a4592b8 | ||
|
|
9dad5e6362 | ||
|
|
f51b5421f2 | ||
|
|
387907ea32 | ||
|
|
b5ca1d48e0 | ||
|
|
396f170738 | ||
|
|
5514039d42 | ||
|
|
c90c4f3673 | ||
|
|
41758858cf | ||
|
|
3ff507e6b8 | ||
|
|
5199371025 | ||
|
|
26ad773f77 | ||
|
|
9e133e0ecc | ||
|
|
c4e7099229 | ||
|
|
56062628f2 | ||
|
|
48a3a1dabb | ||
|
|
ca3c2b3e26 | ||
|
|
d35c5c8320 | ||
|
|
749a582296 | ||
|
|
6310406430 | ||
|
|
d26d339f97 | ||
|
|
17afce6fa1 | ||
|
|
d3aa601798 | ||
|
|
8f24d667d6 | ||
|
|
5cd4588ef2 | ||
|
|
b0a1821d95 | ||
|
|
6b835f70e6 | ||
|
|
308bc76ac6 | ||
|
|
fd843509a1 | ||
|
|
7691cfdadb | ||
|
|
1aa9368e54 | ||
|
|
180e852fee | ||
|
|
547edd7e32 | ||
|
|
0b46c63c31 | ||
|
|
9b69ce1988 | ||
|
|
57cc44f662 | ||
|
|
4eaa9c7ea9 | ||
|
|
0b6cdcc826 | ||
|
|
7bc0a0ca39 | ||
|
|
1ef2074a10 | ||
|
|
0747b2a72a | ||
|
|
64b3095f2c | ||
|
|
ab82af122f | ||
|
|
54fccf2e7c | ||
|
|
05e953db3a | ||
|
|
f726835850 | ||
|
|
38d2809131 | ||
|
|
d7718991a6 | ||
|
|
32c2bff6eb | ||
|
|
555ede03ed | ||
|
|
2a6f218700 | ||
|
|
b087c19b52 | ||
|
|
6fed1c60ac | ||
|
|
b10f2a5291 | ||
|
|
a4a49a354e | ||
|
|
ead23528ca | ||
|
|
b8644949cc | ||
|
|
b88e3a64f2 | ||
|
|
2871b423fd | ||
|
|
562fafbc39 | ||
|
|
191e0874f8 | ||
|
|
fa5ff7bfa5 | ||
|
|
82e80ccdeb | ||
|
|
ff84cf5a18 | ||
|
|
44843a7768 | ||
|
|
68eeb338da | ||
|
|
ea54c17902 | ||
|
|
d5ad7eed40 | ||
|
|
c2430cd3f4 | ||
|
|
8a0731493f | ||
|
|
07e2b80b5d | ||
|
|
1311e1fca2 | ||
|
|
e6c2133520 | ||
|
|
3d6f734ff2 | ||
|
|
e0eaa58779 | ||
|
|
ced6622b25 | ||
|
|
2d2cf96f5e | ||
|
|
370f1b51ee | ||
|
|
67d3a504ae | ||
|
|
34ee48ef93 | ||
|
|
c61a585e79 | ||
|
|
09388c9f07 | ||
|
|
b1bc78dad3 | ||
|
|
a5a0f23c3a | ||
|
|
4c50c064d3 | ||
|
|
a63f271300 | ||
|
|
08b22ff550 | ||
|
|
b04133bf65 | ||
|
|
3602358d2c | ||
|
|
67b0416eba | ||
|
|
f8d69ecb1f | ||
|
|
5e8bc204c1 | ||
|
|
938332d646 | ||
|
|
386078d441 | ||
|
|
4d778faaf4 | ||
|
|
3055cf5602 | ||
|
|
36373ba28b | ||
|
|
1415b57181 | ||
|
|
65d4a4e906 | ||
|
|
0abe221227 | ||
|
|
1b8d822226 | ||
|
|
bc8fbd60d7 | ||
|
|
4c332eac9a | ||
|
|
f5392d77dc | ||
|
|
1e88990a0b | ||
|
|
de4c7567d0 | ||
|
|
aff33dd023 | ||
|
|
a287ebe324 | ||
|
|
583a8d1001 | ||
|
|
36a808add0 | ||
|
|
f651d4a274 | ||
|
|
f6e8e645f2 | ||
|
|
b4d2f88b74 | ||
|
|
c524a736bd | ||
|
|
cdf3bf18f0 | ||
|
|
b21bdacb4f | ||
|
|
92019b5ba3 | ||
|
|
908d635063 | ||
|
|
20329feb7f | ||
|
|
4cd92a1cd9 | ||
|
|
8ca3397c5d | ||
|
|
05cd79481e | ||
|
|
c0f1905785 | ||
|
|
9afc06028d | ||
|
|
7b6cc50238 | ||
|
|
722b1d0258 | ||
|
|
93d3b78ac1 | ||
|
|
69e82f6e0e | ||
|
|
1f83a6db7b | ||
|
|
8f37951e14 | ||
|
|
e1f4d43926 | ||
|
|
eb6139276f | ||
|
|
f18c426151 | ||
|
|
e46debb6d1 | ||
|
|
d8b8739cb8 | ||
|
|
9fd8eefa59 | ||
|
|
fd2af06a53 | ||
|
|
48f945ba7f | ||
|
|
6d59f88a76 | ||
|
|
8b94d4626d | ||
|
|
d7d8e78e42 | ||
|
|
9755f86f53 | ||
|
|
599a3ee82f | ||
|
|
c790346490 | ||
|
|
68cf3a9111 | ||
|
|
59221dfcf2 | ||
|
|
020413a206 | ||
|
|
a343e9ebdf | ||
|
|
c304efee36 | ||
|
|
95173f676d | ||
|
|
87d1db1f2b | ||
|
|
d445f384c7 | ||
|
|
59dd4b0721 | ||
|
|
d73c91d4a7 | ||
|
|
665a66522e | ||
|
|
ba1efa65fa | ||
|
|
64a74692b9 | ||
|
|
46cd285dd0 | ||
|
|
bcac115c3d | ||
|
|
77ddb2c8c2 | ||
|
|
8ae31eb998 | ||
|
|
7551a6ae1d | ||
|
|
93be659b1b | ||
|
|
3327878fc2 | ||
|
|
0b25c7f3c1 | ||
|
|
b606ba4dd7 | ||
|
|
0269bc810c | ||
|
|
f2775f2c1d | ||
|
|
f06274aec6 | ||
|
|
dfa686a3e0 | ||
|
|
fe679b5de5 | ||
|
|
aa1eb0410c | ||
|
|
1b06bab7ee | ||
|
|
0f13056aa2 | ||
|
|
beecf60db7 | ||
|
|
160a2013bc | ||
|
|
b8c636e87e | ||
|
|
add40e5926 | ||
|
|
960e7f3b24 | ||
|
|
3e749f36e8 | ||
|
|
8198d99309 | ||
|
|
81de1c8ed4 | ||
|
|
3eb55e9d9b | ||
|
|
6b6ac86aea | ||
|
|
1b45e70cbb | ||
|
|
929f7ec235 | ||
|
|
cf28d7e470 | ||
|
|
b0a00108f3 | ||
|
|
01151fc7f8 | ||
|
|
bf8dbc38c7 | ||
|
|
ae0d4d9353 | ||
|
|
43ec4ae238 | ||
|
|
c568a8cabe | ||
|
|
423bdd8b6b | ||
|
|
1e19ff65e6 | ||
|
|
a9cf632f53 | ||
|
|
fddf94a341 | ||
|
|
203168d261 | ||
|
|
0e3c3536f8 | ||
|
|
45b2b7e24f | ||
|
|
88f177b065 | ||
|
|
ea7a8dd3ad | ||
|
|
dda8f92494 | ||
|
|
26211802cd | ||
|
|
b4bef0d32c | ||
|
|
a8bf203067 | ||
|
|
624dd2e99d | ||
|
|
26a136a6e8 | ||
|
|
2d57ffa546 | ||
|
|
428b81a6d9 | ||
|
|
f24c12fdfb | ||
|
|
9a345d191b | ||
|
|
fec706d134 | ||
|
|
c338448997 | ||
|
|
956c1cb1a7 | ||
|
|
8831b4da9e | ||
|
|
f8bd60dcd7 | ||
|
|
6a373b585c | ||
|
|
54480e2510 | ||
|
|
83f73603db | ||
|
|
2b10f192ba | ||
|
|
775d5289cb | ||
|
|
e6c66352a7 | ||
|
|
77afd73ee1 | ||
|
|
5ac96a40aa | ||
|
|
2fea0e2598 | ||
|
|
2b64949cbe | ||
|
|
918d57f25e | ||
|
|
5e2b40d3a9 | ||
|
|
979ef4bd43 | ||
|
|
914baf594b | ||
|
|
02b0802886 | ||
|
|
0725239397 | ||
|
|
d72e8a06a7 | ||
|
|
cf79fec720 | ||
|
|
31dda45d1c | ||
|
|
9836b72661 | ||
|
|
6ede0194c6 | ||
|
|
5ec38581ca | ||
|
|
2629e59ace | ||
|
|
f3eb770e20 | ||
|
|
0683b79fac | ||
|
|
fcd09e2bae | ||
|
|
b25cb0f2d5 | ||
|
|
0704aec01b | ||
|
|
d38b939c63 | ||
|
|
112466de1e | ||
|
|
1d0edd2ad0 | ||
|
|
37736119be | ||
|
|
c5df150acb | ||
|
|
49a346334b | ||
|
|
e7c4656e8f | ||
|
|
85bed0582a | ||
|
|
b9e6f867c6 | ||
|
|
a5b80168bd | ||
|
|
3cea5fb2d0 | ||
|
|
c89d782048 | ||
|
|
1fd4179e07 | ||
|
|
3b62400298 | ||
|
|
ba0fe938a1 | ||
|
|
d4560171a8 | ||
|
|
9963ba6cf5 | ||
|
|
f5a23826c8 | ||
|
|
21ff005c1a | ||
|
|
206b9135f1 | ||
|
|
5449de1a67 | ||
|
|
47c61756e6 | ||
|
|
ef63679ff0 | ||
|
|
ef7e1c385a | ||
|
|
3a0a6cdbbb | ||
|
|
340df3e956 | ||
|
|
eeb264da8e | ||
|
|
00f08b8ec6 | ||
|
|
67ae2a39ba | ||
|
|
72dda25835 | ||
|
|
8c9ce1994a | ||
|
|
606b05fdaf | ||
|
|
420ba9549f | ||
|
|
51fbb5cfac | ||
|
|
ca2c2b60f2 | ||
|
|
d6064636d4 | ||
|
|
9646c9b0a0 | ||
|
|
f28900f8ca | ||
|
|
e0c15c42d7 | ||
|
|
7177618c25 | ||
|
|
3bdd4af75c | ||
|
|
98d4b5e487 | ||
|
|
8b5639bfdb | ||
|
|
1c70b8b1bf | ||
|
|
b5a7e03879 | ||
|
|
a6dade846e | ||
|
|
32913f9d95 | ||
|
|
cbabf9a2a3 | ||
|
|
03e92c3336 | ||
|
|
997c39fdc0 | ||
|
|
bba2d1ee18 | ||
|
|
a60301810f | ||
|
|
6b261f76b1 | ||
|
|
3db5f21d90 | ||
|
|
121bfcb984 | ||
|
|
265de66946 | ||
|
|
79c5c9f95e | ||
|
|
3354a47e8a | ||
|
|
a1e1416d7a | ||
|
|
24b7a9907f | ||
|
|
214abd0cd4 | ||
|
|
0bb53e8ca5 | ||
|
|
ec12caaeba | ||
|
|
5bbf2aa57a | ||
|
|
84ad9997da | ||
|
|
9f6ce87443 | ||
|
|
1ff6c382bf | ||
|
|
c366c10ae1 | ||
|
|
950df1e25c | ||
|
|
909bfa8c14 | ||
|
|
598b48d078 | ||
|
|
4c7b7f236a | ||
|
|
896c5b41cb | ||
|
|
205c35d5e5 | ||
|
|
bf0dd7d725 | ||
|
|
ba2b811172 | ||
|
|
be347c3ed4 | ||
|
|
c01abdb6a9 | ||
|
|
95a5a965a5 | ||
|
|
fc2849a8ff | ||
|
|
fcc900f3e0 | ||
|
|
9d0bcd5637 | ||
|
|
6ebbad5273 | ||
|
|
930459899a | ||
|
|
fe730e2d76 | ||
|
|
e58b2e9a47 | ||
|
|
719dbab0c2 | ||
|
|
86ea023e2e | ||
|
|
bc0fb3f44c | ||
|
|
6d37f6b4dd | ||
|
|
958ab85297 | ||
|
|
facef1d23c | ||
|
|
cdb446375c | ||
|
|
6f7b2887aa | ||
|
|
bc8ae063dd | ||
|
|
a23f973433 | ||
|
|
c124256bad | ||
|
|
f1b035bcca | ||
|
|
81cc7c591e | ||
|
|
a0ca560c3b | ||
|
|
d1f20a9c73 | ||
|
|
013059c5b9 | ||
|
|
fe6ad235ac | ||
|
|
67af26ffe6 | ||
|
|
0fce9de04f | ||
|
|
a8b8df21d6 | ||
|
|
ab2e304f02 | ||
|
|
574f2c53d4 | ||
|
|
bc85d812d2 | ||
|
|
364990a29f | ||
|
|
9ac9e36873 | ||
|
|
6745e09688 | ||
|
|
e758927c84 | ||
|
|
90fbf7d50f | ||
|
|
75f1ee0082 | ||
|
|
750aaf451a | ||
|
|
b44bfe9109 | ||
|
|
aa392f8563 | ||
|
|
ac7228f6c4 | ||
|
|
dcb12addaa | ||
|
|
ad398b5f8a | ||
|
|
803cb4806e | ||
|
|
1a468bbb61 | ||
|
|
c332c051f3 | ||
|
|
db48befcb7 | ||
|
|
b02edb05ac | ||
|
|
d7009fd1c8 | ||
|
|
ce3755676c | ||
|
|
db3c1b728d | ||
|
|
1761de4740 | ||
|
|
09d496925b | ||
|
|
3af5327f1c | ||
|
|
06cb14d7ec | ||
|
|
7be1a9d7fa | ||
|
|
95243fb35c | ||
|
|
26438a3979 | ||
|
|
28db097a71 | ||
|
|
76fdd8ae04 | ||
|
|
003dc39d76 | ||
|
|
3a73bfb142 | ||
|
|
a24bdabf08 | ||
|
|
8815f293a2 | ||
|
|
0b04c28011 | ||
|
|
9af2775539 | ||
|
|
ae5deae6e9 | ||
|
|
61c2126498 | ||
|
|
881fe0cfce | ||
|
|
a999bf389c | ||
|
|
ff3b97b630 | ||
|
|
639b520d39 | ||
|
|
31854ad9e8 | ||
|
|
4304e92f0d | ||
|
|
44736cefbf | ||
|
|
a807449f28 | ||
|
|
19dc29ea17 | ||
|
|
ae9d38b59c | ||
|
|
97bebae8d7 | ||
|
|
cf534ce6da | ||
|
|
f25f691a55 | ||
|
|
cbebf8be7b | ||
|
|
165ce26b2f | ||
|
|
05e50c1b98 | ||
|
|
e11004da7b | ||
|
|
97ee7b5d96 | ||
|
|
6c6e2573aa | ||
|
|
8992827f21 | ||
|
|
572c863bff | ||
|
|
d3c85d67b8 | ||
|
|
ff3434f77e | ||
|
|
762528c044 | ||
|
|
1891af0d4a | ||
|
|
583ad036f7 | ||
|
|
893b9c1b38 | ||
|
|
ac92bf98d4 | ||
|
|
fd90ff7ff7 | ||
|
|
d06dbbe5db | ||
|
|
bffc4995d7 | ||
|
|
4b8b406bed | ||
|
|
5641153272 | ||
|
|
08c6c7781f | ||
|
|
ad2ad391a7 | ||
|
|
caac88be0d | ||
|
|
10660aa373 | ||
|
|
cfaf97aee4 | ||
|
|
55f14576f0 | ||
|
|
4dca94ac99 | ||
|
|
14779d3d27 | ||
|
|
66d1e710b5 | ||
|
|
12ae3c17e9 | ||
|
|
36e4da0709 | ||
|
|
91631cb081 | ||
|
|
224f082e1f | ||
|
|
76b239a6ea | ||
|
|
cb476b510d | ||
|
|
334f233968 | ||
|
|
e1f21250b9 | ||
|
|
8d087e4f20 | ||
|
|
9e35e8c947 | ||
|
|
f98faef024 | ||
|
|
0f9346ead5 | ||
|
|
fc8118c694 | ||
|
|
6f8166ca0f | ||
|
|
026b7278c1 | ||
|
|
375a7e7e5c | ||
|
|
64db035d77 | ||
|
|
cb051e4254 | ||
|
|
20e9c2901d | ||
|
|
ce4b391495 | ||
|
|
7d932f5b18 | ||
|
|
56c8c08e08 | ||
|
|
13ef25c0b5 | ||
|
|
7a1aecb1a4 | ||
|
|
b3690e8680 | ||
|
|
97d490cfb4 | ||
|
|
2081dda6fc | ||
|
|
ea912fc50c | ||
|
|
b655fdf68f | ||
|
|
4749f46a4f | ||
|
|
f95bb9c82d | ||
|
|
6529529cdb | ||
|
|
3a2a3f21d4 | ||
|
|
631bc1c481 | ||
|
|
6abfa5bf80 | ||
|
|
20ae8e4f8b | ||
|
|
f595c5d504 | ||
|
|
972779253c | ||
|
|
9179a241e9 | ||
|
|
85e7e89ad9 | ||
|
|
9f5bc00c89 | ||
|
|
0ab842e3c5 | ||
|
|
3a0a3e49dc | ||
|
|
438b1c043e | ||
|
|
6d13b65e7c | ||
|
|
423d41ee0e | ||
|
|
1b61d9bc51 | ||
|
|
bf0c3d42db | ||
|
|
5394a68314 | ||
|
|
597af36759 | ||
|
|
691062f687 | ||
|
|
6651c4ea48 | ||
|
|
116559e5a0 | ||
|
|
7818e8ed64 | ||
|
|
b0063399bb | ||
|
|
724c6b7656 | ||
|
|
0530cbcd0f | ||
|
|
6e2bba1513 | ||
|
|
89ac5d7c42 | ||
|
|
dd68722b66 | ||
|
|
933d5db2ab | ||
|
|
0386ed6409 | ||
|
|
d3c14bf416 | ||
|
|
2a224ce9fb | ||
|
|
78322525b7 | ||
|
|
5b7c9c205e | ||
|
|
07ac70a460 | ||
|
|
3629f806a2 | ||
|
|
72fc43c738 | ||
|
|
2880391620 | ||
|
|
df38f0be3f | ||
|
|
808684c2a8 | ||
|
|
69ed3a7751 | ||
|
|
68556caa9a | ||
|
|
bb8ea8053b | ||
|
|
6f01e6edf1 | ||
|
|
66a74d16ff | ||
|
|
0e525f5aeb | ||
|
|
f742605a1b | ||
|
|
86007c466d | ||
|
|
7b39b79183 | ||
|
|
7f453aa6f6 | ||
|
|
f36052ffeb | ||
|
|
d35db11f43 | ||
|
|
173e5d3f97 | ||
|
|
bcebf737c3 | ||
|
|
2afff67e89 | ||
|
|
fe7bd53250 | ||
|
|
6df3509869 | ||
|
|
062dc771aa | ||
|
|
5280b72b85 | ||
|
|
a4dfe7138e | ||
|
|
b9960bad1a | ||
|
|
e1d7805396 | ||
|
|
ce6f993f0e | ||
|
|
aa1d94e6c9 | ||
|
|
00a6092836 | ||
|
|
f6372faa23 | ||
|
|
1a5e266d26 | ||
|
|
2e4a107201 | ||
|
|
d4688db31c | ||
|
|
c49a289619 | ||
|
|
2559496ded | ||
|
|
d3aa82fc5d | ||
|
|
704605918d | ||
|
|
7e8116888b | ||
|
|
e0f4bbd20d | ||
|
|
5ae2d5a24b | ||
|
|
8648737a7a | ||
|
|
6e090d5112 | ||
|
|
2207e561f2 | ||
|
|
b9cd5b572b | ||
|
|
344c6f3ee7 | ||
|
|
f6f2c0ed56 | ||
|
|
ec7a66a966 | ||
|
|
23ca428a01 | ||
|
|
eb9f251e34 | ||
|
|
9c3a0c86ca | ||
|
|
394d875eb4 | ||
|
|
4cc6403867 | ||
|
|
9d5fa773f3 | ||
|
|
075ca49a1f | ||
|
|
9564f1d871 | ||
|
|
cf546a47b6 | ||
|
|
d3a4f35170 | ||
|
|
f450aea449 | ||
|
|
aed308b259 | ||
|
|
714dffc943 | ||
|
|
f8a173efbd | ||
|
|
70e7822dd1 | ||
|
|
01b24e984c | ||
|
|
9dd4302fe9 | ||
|
|
c2f03aa833 | ||
|
|
2260459422 | ||
|
|
452631550a | ||
|
|
a14266b452 | ||
|
|
e838cc3fe9 | ||
|
|
74457b95e9 | ||
|
|
ceb19c7573 | ||
|
|
d5754515a6 | ||
|
|
4ed13c04f5 | ||
|
|
5a5294cc44 | ||
|
|
3a5d48ae7c | ||
|
|
6b605804d2 | ||
|
|
ffe883ab72 | ||
|
|
31c4e0fdfe | ||
|
|
66f970e0bd | ||
|
|
07b223dcb0 | ||
|
|
f1e27b6ffb | ||
|
|
389463aea5 | ||
|
|
b11ab9a31c | ||
|
|
5fe85b07b7 | ||
|
|
3c7b1e7d21 | ||
|
|
c556cf1e69 | ||
|
|
722e5fb5f7 | ||
|
|
e90cc591b7 | ||
|
|
7336d327b3 | ||
|
|
c555725201 | ||
|
|
c9c5225a6a | ||
|
|
e1060bf537 | ||
|
|
392e42a55d | ||
|
|
b974e41148 | ||
|
|
aa3e2a0b64 | ||
|
|
3df89dd9a3 | ||
|
|
cef1f3c7ee | ||
|
|
e5d1fa1ea4 | ||
|
|
3ccf2937b4 | ||
|
|
b7b696630f | ||
|
|
84aba546bc | ||
|
|
6ef751422a | ||
|
|
05d49ee45e | ||
|
|
3e4783c454 | ||
|
|
ce93201843 | ||
|
|
f9fc3a46b5 | ||
|
|
0467d6666a | ||
|
|
1f26b076a3 | ||
|
|
7944f21925 | ||
|
|
e91eda8eca | ||
|
|
d8ac84a5da | ||
|
|
3098a02b72 | ||
|
|
741236df56 | ||
|
|
e3584f0a61 | ||
|
|
432482c5a7 | ||
|
|
323ad46bba | ||
|
|
ace0fe1802 | ||
|
|
36f244fece | ||
|
|
99e5ef99ec | ||
|
|
d6d3ed5bbc | ||
|
|
49d09ecf30 | ||
|
|
c529a03096 | ||
|
|
101d9aa0fa | ||
|
|
b4864e1180 | ||
|
|
cba7304ab9 | ||
|
|
2d058b0519 | ||
|
|
eed9231884 | ||
|
|
5c84eaf2a5 | ||
|
|
2ef7226be0 | ||
|
|
e5d1c61cdf | ||
|
|
e635055ab8 | ||
|
|
d8d4e3b262 | ||
|
|
8f29543479 | ||
|
|
c11a8ea24b | ||
|
|
86646d7faa | ||
|
|
6e44915e08 | ||
|
|
f8b99cf4e9 | ||
|
|
0e7d6ff192 | ||
|
|
66501dac97 | ||
|
|
195907b2ec | ||
|
|
be11b44864 | ||
|
|
bc7cd21c13 | ||
|
|
0555cbdd28 | ||
|
|
97498451bb | ||
|
|
2e0d9b5475 | ||
|
|
62395f3103 | ||
|
|
6b31cd6aee | ||
|
|
e67a8ba369 | ||
|
|
133d301925 | ||
|
|
17c40a5d1d | ||
|
|
042211e5f6 | ||
|
|
d12830d700 | ||
|
|
b411c70280 | ||
|
|
2bc22cc7d5 | ||
|
|
b4c189c89b | ||
|
|
fe3f351a2d | ||
|
|
076be9cba7 | ||
|
|
f28dcd85fb | ||
|
|
8e0f17c9d7 | ||
|
|
d73817a0db | ||
|
|
11874db825 | ||
|
|
5d5a43ce90 | ||
|
|
75e548caab | ||
|
|
c901e7ba06 | ||
|
|
b1dc3dfca1 | ||
|
|
ce4ed20c69 | ||
|
|
d44df7f860 | ||
|
|
54353e0e1f | ||
|
|
1c042b6efb | ||
|
|
b8df1f29c4 | ||
|
|
18252f5b03 | ||
|
|
881370f284 | ||
|
|
35d1747bc3 | ||
|
|
91ac89a54e | ||
|
|
3c694d9bd9 | ||
|
|
6a78e9df77 | ||
|
|
078c6eb30a | ||
|
|
d35c6002a6 | ||
|
|
f23fc3beaa | ||
|
|
5a352e5ace | ||
|
|
27cae0065e | ||
|
|
50be2993fa | ||
|
|
d9ea15e9bc | ||
|
|
03b1d45d7e | ||
|
|
e48d6878c4 | ||
|
|
1a3b255848 | ||
|
|
f00aa94d7e | ||
|
|
f7980b19f4 | ||
|
|
6a1f9677a5 | ||
|
|
e844bb36a5 | ||
|
|
ae364adfc2 | ||
|
|
c14a382b90 | ||
|
|
da9c2beaaf | ||
|
|
a4a56476db | ||
|
|
39d3784b9b | ||
|
|
7d29df58f1 | ||
|
|
05aa413683 | ||
|
|
132f90f45b | ||
|
|
4526fd1917 | ||
|
|
2602dc15b0 | ||
|
|
2314f63424 | ||
|
|
c2410600ee | ||
|
|
f7e4702685 | ||
|
|
71682512c4 | ||
|
|
20b82fbf77 | ||
|
|
631f09847d | ||
|
|
671585f68a | ||
|
|
5feed888bb | ||
|
|
47bef0f1b0 | ||
|
|
c1dc662a40 | ||
|
|
16e1721fe8 | ||
|
|
f406e13600 | ||
|
|
9063133a7b | ||
|
|
3fbb436187 | ||
|
|
7c845d070b | ||
|
|
5e905aa73e | ||
|
|
e15654f265 | ||
|
|
369c067efc | ||
|
|
c037e69793 | ||
|
|
8c935ff44e | ||
|
|
74bf8b0554 | ||
|
|
6345972efe | ||
|
|
16242e87a1 | ||
|
|
8155320ba5 | ||
|
|
39a7f63972 | ||
|
|
7b72209277 | ||
|
|
cad20a0bfe | ||
|
|
ba8d79f202 | ||
|
|
176fe699b9 | ||
|
|
213ee7be13 | ||
|
|
48fd869c71 | ||
|
|
53e47484e2 | ||
|
|
dc18b20e5a | ||
|
|
b4c5debbdf | ||
|
|
771d3d52b9 | ||
|
|
2a53ac093d | ||
|
|
4fa2af72cc | ||
|
|
e512d3cd61 | ||
|
|
16b7ee3985 | ||
|
|
4f3d26c31b | ||
|
|
587bf94d69 | ||
|
|
635ad77e0d | ||
|
|
33258da6c3 | ||
|
|
c2b3c3379d | ||
|
|
e30fac02ed | ||
|
|
e74592a654 | ||
|
|
ebd47aa73b | ||
|
|
e2d19cbaba | ||
|
|
1f864a846f | ||
|
|
fc32c2c944 | ||
|
|
279e37f1cb | ||
|
|
3f9176176e | ||
|
|
b3201ccafd | ||
|
|
2a01a620a9 | ||
|
|
6f43cf7b82 | ||
|
|
1c6250f9c2 | ||
|
|
650075a9ab | ||
|
|
668ee6f24a | ||
|
|
c456ec2c4c | ||
|
|
3b0c390a9e | ||
|
|
b02eb11345 | ||
|
|
ed3ba303bc | ||
|
|
ee5da1410e | ||
|
|
494627c6e1 | ||
|
|
82ac112dec | ||
|
|
40cfff33ff | ||
|
|
c1c5873912 | ||
|
|
c090efd562 | ||
|
|
91dbb0e77b | ||
|
|
dde7b144f0 | ||
|
|
f1873a014c | ||
|
|
48b8923b67 | ||
|
|
6f9f3fea83 | ||
|
|
10f3320165 | ||
|
|
d57aa33b40 | ||
|
|
0e7c78bae3 | ||
|
|
e6602786ec | ||
|
|
31bbb3894c | ||
|
|
8bbf576807 | ||
|
|
1ecc9b9d0e | ||
|
|
3adbebc3d9 | ||
|
|
a4c086f51b | ||
|
|
0ecdcec698 | ||
|
|
ae7f026d46 | ||
|
|
2813e0b706 | ||
|
|
a409562d00 | ||
|
|
b6b6661ea1 | ||
|
|
fb7b6f667c | ||
|
|
b94efe81e4 | ||
|
|
72a1b3d2a0 | ||
|
|
20bff18bd4 | ||
|
|
ba5c5ef4fb | ||
|
|
aff3ac0bb3 | ||
|
|
2c350daf01 | ||
|
|
cb7627c736 | ||
|
|
f731a5cae4 | ||
|
|
07cb7c9305 | ||
|
|
86e9cc4896 | ||
|
|
12c9405257 | ||
|
|
4708b44c59 | ||
|
|
4cb428bb92 | ||
|
|
d7391b19bc | ||
|
|
db9e56d1ca | ||
|
|
e527af10f4 | ||
|
|
74c7be0a6d | ||
|
|
2d40e7b51a | ||
|
|
ea407fb2ea | ||
|
|
fca5b0529a | ||
|
|
65fd1dd2d8 | ||
|
|
0a7ede3818 | ||
|
|
24e84f3866 | ||
|
|
c1c8e46c09 | ||
|
|
8591e4f301 | ||
|
|
10db7ad89c | ||
|
|
4ca4f442b9 | ||
|
|
6d5ecbc9c4 | ||
|
|
ea685a5686 | ||
|
|
376d3b6e91 | ||
|
|
df7397af1f | ||
|
|
9ba9345b04 | ||
|
|
9fc5758ae0 | ||
|
|
25298a6182 | ||
|
|
246da1bff6 | ||
|
|
8b5d12b958 | ||
|
|
3817370d4e | ||
|
|
c29846a9da | ||
|
|
2158f906a7 | ||
|
|
008dd280d6 | ||
|
|
fb99db011c | ||
|
|
c0fbd9cb3c | ||
|
|
fb79b18002 | ||
|
|
3841f22108 | ||
|
|
379d523ac5 | ||
|
|
07ec7f8c13 | ||
|
|
d0f29cc7a2 | ||
|
|
0e23a487f7 | ||
|
|
ac10f7c426 | ||
|
|
852c00009e | ||
|
|
b365798e66 | ||
|
|
66a6097a49 | ||
|
|
0e529d3d92 | ||
|
|
06c75dd656 | ||
|
|
69c32d4d90 | ||
|
|
36ffebd975 | ||
|
|
deb56539fb | ||
|
|
af8d099b9f | ||
|
|
eed869d321 | ||
|
|
f8f2acf6c3 | ||
|
|
7be9f74827 | ||
|
|
ed77982330 | ||
|
|
e1b47eca90 | ||
|
|
bfa0b2d2bc | ||
|
|
d67783edbd | ||
|
|
77712b6664 | ||
|
|
1633e7faa6 | ||
|
|
2dc5ee5862 | ||
|
|
bbaea23eea | ||
|
|
d112ac7eef | ||
|
|
cf21e987f2 | ||
|
|
dae11765bc | ||
|
|
df30a3c7f4 | ||
|
|
aaa4600597 | ||
|
|
ba1730d26b | ||
|
|
d1b5c812f7 | ||
|
|
5bfe6d1c84 | ||
|
|
9804a794fd | ||
|
|
0344645208 | ||
|
|
f56ad6e787 | ||
|
|
7492db592b | ||
|
|
2e80ecf8a7 | ||
|
|
6993551a44 | ||
|
|
9bff15adfe | ||
|
|
0c24da2358 | ||
|
|
4ee4adb43d | ||
|
|
e33b028348 | ||
|
|
384d9c1841 | ||
|
|
f78fcd6b5f | ||
|
|
711a72989b | ||
|
|
d62a2fc1d5 | ||
|
|
f33f712a3a | ||
|
|
3315b3310b | ||
|
|
c7134b16ed | ||
|
|
f36f710661 | ||
|
|
00ab8681c7 | ||
|
|
4137bbac94 | ||
|
|
750b2ad599 | ||
|
|
511c833fbb | ||
|
|
29063a0c07 | ||
|
|
67909b3557 | ||
|
|
102c12d36c | ||
|
|
dc51651665 | ||
|
|
8b563d6d5f | ||
|
|
eb8b88027e | ||
|
|
a5b7f307ed | ||
|
|
45fca425aa | ||
|
|
a0bd9b5fd7 | ||
|
|
c12e24e3e3 | ||
|
|
d147c5a153 | ||
|
|
7a833456d9 | ||
|
|
306f33dfc4 | ||
|
|
a2745a4060 | ||
|
|
46b68c7b2a | ||
|
|
4264760113 | ||
|
|
42dedbbd9a | ||
|
|
ea99bfdb16 | ||
|
|
2ccb934338 | ||
|
|
367027cfbc | ||
|
|
c4ebd3b6d5 | ||
|
|
5f8cd82a09 | ||
|
|
0ef15fa662 | ||
|
|
c05452af91 | ||
|
|
4c8bafcf9a | ||
|
|
034f30a49a | ||
|
|
0f13075e17 | ||
|
|
ad274a5981 | ||
|
|
fdbcbaec8a | ||
|
|
9c09072ee6 | ||
|
|
0a4ca56da9 | ||
|
|
2b494398f2 | ||
|
|
95585c2264 | ||
|
|
92f9f0da9e | ||
|
|
fe943b5e95 | ||
|
|
3479a4661a | ||
|
|
7ba438cf7c | ||
|
|
c761e9fba0 | ||
|
|
deae31ea4a | ||
|
|
547355a163 | ||
|
|
9be4cb132c | ||
|
|
3e25c9f3f4 | ||
|
|
220c7e9139 | ||
|
|
79a085a9be | ||
|
|
b505c5a9d3 | ||
|
|
03ba660ea1 | ||
|
|
5aeb361f6d | ||
|
|
0e519a4e97 | ||
|
|
4feee00d34 | ||
|
|
ef5e4cdc0a | ||
|
|
67c599f50b | ||
|
|
5af9b61357 | ||
|
|
1d6771b4ed | ||
|
|
c55dc0a58e | ||
|
|
c525d55db8 | ||
|
|
408f66ef80 | ||
|
|
7f73a047a4 | ||
|
|
015bc98d60 | ||
|
|
5cd9ba609a | ||
|
|
c8ed6544db | ||
|
|
1162113d3b | ||
|
|
1612503e7b | ||
|
|
34ba85df3b | ||
|
|
8206e7d2a2 | ||
|
|
3e29672c70 | ||
|
|
f67aaafe4f | ||
|
|
ed704afc07 | ||
|
|
bbbfacb4b2 | ||
|
|
cf16d2e52d | ||
|
|
6268e6e1c9 | ||
|
|
99b8e5b303 | ||
|
|
73446af330 | ||
|
|
a0b917a207 | ||
|
|
53ec7edd06 | ||
|
|
ff804c0ff8 | ||
|
|
9d69ff01b3 | ||
|
|
61831f530f | ||
|
|
6065db1d24 | ||
|
|
270b8415e8 | ||
|
|
1987331a70 | ||
|
|
ab85216b96 | ||
|
|
b5cb78c77e | ||
|
|
e75c99672d | ||
|
|
7faba72ebe | ||
|
|
cbe8fc1bdf | ||
|
|
f66a7660e2 | ||
|
|
5f3159a203 | ||
|
|
76aeda4436 | ||
|
|
fa791cd28c | ||
|
|
d836c68ee5 | ||
|
|
519d90f0a7 | ||
|
|
26400be6f7 | ||
|
|
92ba7e9d54 | ||
|
|
25eafe1e69 | ||
|
|
118a9ca861 | ||
|
|
174a1fe834 | ||
|
|
1e0f3f40a9 | ||
|
|
19623694f5 | ||
|
|
55a16043e1 | ||
|
|
29943b7edd | ||
|
|
a1e2af9533 | ||
|
|
c350e2a668 | ||
|
|
e0868ba2ab | ||
|
|
bcefd1fbd8 | ||
|
|
f5fbad0abf | ||
|
|
95b1a197be | ||
|
|
39169a36f5 | ||
|
|
3b1a409f07 | ||
|
|
accd70d4b4 | ||
|
|
3c7f4b760f | ||
|
|
f7d7ccfd2c | ||
|
|
de98a03887 | ||
|
|
0e11c240cb | ||
|
|
c0a298e484 | ||
|
|
907b9a9862 | ||
|
|
8d70d91cf4 | ||
|
|
6fb86859ce | ||
|
|
fe733b319f | ||
|
|
08b58f3055 | ||
|
|
9f6659f526 | ||
|
|
d28397df93 | ||
|
|
2e1c37146b | ||
|
|
903adc8f97 | ||
|
|
fc7f454382 | ||
|
|
ef35fc63a1 | ||
|
|
52a193b183 | ||
|
|
dad401a6ec | ||
|
|
ec3f8118db | ||
|
|
cfc0194c00 | ||
|
|
dd28f52301 | ||
|
|
9dcbd532e6 | ||
|
|
16b4a2cad2 | ||
|
|
bd9d8a035a | ||
|
|
d55ffb0212 | ||
|
|
e76ddfd005 | ||
|
|
59145a3b40 | ||
|
|
c993f5343b | ||
|
|
b62acec5ee | ||
|
|
b34ab9cdd5 | ||
|
|
e0a8ab852e | ||
|
|
bd5ebd0e41 | ||
|
|
661e21d25b | ||
|
|
dc69a2bc94 | ||
|
|
e6fec6b27d | ||
|
|
27b92f9838 | ||
|
|
3446def4dd | ||
|
|
2700206715 | ||
|
|
fdfc6f70f3 | ||
|
|
065c288edb | ||
|
|
3121d2c23e | ||
|
|
7cd3bb524c | ||
|
|
6b8cc97779 | ||
|
|
b7112a1edd | ||
|
|
28965b7356 | ||
|
|
bd3aa28416 | ||
|
|
9fed4ce24c | ||
|
|
90383e30dd | ||
|
|
13f184e957 | ||
|
|
a7a2c6d7ff | ||
|
|
d1bf39d0ac | ||
|
|
7dff6b8415 | ||
|
|
656e019829 | ||
|
|
2133b0f498 | ||
|
|
bc4dcee2b1 | ||
|
|
0e8cf95739 | ||
|
|
e133290c57 | ||
|
|
1429b0677e | ||
|
|
d03ee36647 | ||
|
|
6e5880c642 | ||
|
|
fa93cffafb | ||
|
|
ce9af0fb57 | ||
|
|
95700d8d51 | ||
|
|
fb15e98519 | ||
|
|
3054cb7971 | ||
|
|
f84587cf5a | ||
|
|
538f38f314 | ||
|
|
06578349c7 | ||
|
|
a807476171 | ||
|
|
7aacf9ca89 | ||
|
|
50dae5fb83 | ||
|
|
0853c425fe | ||
|
|
edcc676693 | ||
|
|
c8a742a121 | ||
|
|
ee14a7e15f | ||
|
|
e1eaccf409 | ||
|
|
d2aae52868 | ||
|
|
9158fb4745 | ||
|
|
0ff5ef5337 | ||
|
|
1ace145f85 | ||
|
|
565eb4b450 | ||
|
|
f39861c43f | ||
|
|
72838c248f | ||
|
|
9be8765ccf | ||
|
|
48732c4393 | ||
|
|
5bf0b7c920 | ||
|
|
51a4580d0f | ||
|
|
266e611afa | ||
|
|
22598b693c | ||
|
|
008fe38f10 | ||
|
|
24e8123240 | ||
|
|
6054f03c0d | ||
|
|
476f6d83e2 | ||
|
|
ec57109f39 | ||
|
|
d73488f887 | ||
|
|
3201380c29 | ||
|
|
1f04b39ae3 | ||
|
|
9e2b47027c | ||
|
|
662149a98a | ||
|
|
fafa31589d | ||
|
|
43a5940b9e | ||
|
|
33908624fd | ||
|
|
ffef8a341f | ||
|
|
a48c5df844 | ||
|
|
37b6a668ab | ||
|
|
2a9a7cc897 | ||
|
|
c62e6b5734 | ||
|
|
6ec1d0b935 | ||
|
|
6c5769fdd8 | ||
|
|
09acc5920c | ||
|
|
9613c58bf8 | ||
|
|
147f9ac64b | ||
|
|
cc1e8961a1 | ||
|
|
3b1b2f401d | ||
|
|
58a87b9b61 | ||
|
|
f09475a6b5 | ||
|
|
750422d858 | ||
|
|
03d911d335 | ||
|
|
2747c11a46 | ||
|
|
5a9fe2637f | ||
|
|
a5787f9988 | ||
|
|
85e22bb515 | ||
|
|
5e1e90ad80 | ||
|
|
fe85421c7e | ||
|
|
38af6ce279 | ||
|
|
fe92f16da4 | ||
|
|
edc4b562f2 | ||
|
|
7b6a91064c | ||
|
|
a32414c6fc | ||
|
|
259b4e29de | ||
|
|
e56f80b546 | ||
|
|
1ff52c5290 | ||
|
|
70bd249f43 | ||
|
|
f2f7e43579 | ||
|
|
c36d60fcd4 | ||
|
|
0950f307d1 | ||
|
|
a9b7e4a85e | ||
|
|
912a886539 | ||
|
|
f7484f49e5 | ||
|
|
9f68be1925 | ||
|
|
ef298f8d7a | ||
|
|
c038f0e6ee | ||
|
|
3c53a93447 | ||
|
|
7e86e65cce | ||
|
|
ad171d6cbb | ||
|
|
76ffc20836 | ||
|
|
c4cc5b6dfc | ||
|
|
878c9210d3 | ||
|
|
35c982b367 | ||
|
|
9c4ff080af | ||
|
|
d32ca64a03 | ||
|
|
53b89390be | ||
|
|
a8e09d7fe6 | ||
|
|
0c7cf20e7e | ||
|
|
6ebcee33c5 | ||
|
|
c73544fb59 | ||
|
|
37c1f93bcb | ||
|
|
95aa2e10fc | ||
|
|
279e6e3925 | ||
|
|
8a661d5ee4 | ||
|
|
67fcb40455 | ||
|
|
641524c80e | ||
|
|
55802354d4 | ||
|
|
75f4f5c0bd | ||
|
|
382e9dee55 | ||
|
|
6861d4029e | ||
|
|
370ef16854 | ||
|
|
9dbe434792 | ||
|
|
21a0e95623 | ||
|
|
57c6307479 | ||
|
|
01d8b1f468 | ||
|
|
ef6b82a0a3 | ||
|
|
19b7d1a7c5 | ||
|
|
097d901191 | ||
|
|
a1b5846b29 | ||
|
|
dbdb353e69 | ||
|
|
4456eac1fd | ||
|
|
ba3f8f432e | ||
|
|
bab539f52c | ||
|
|
42dbb128be | ||
|
|
5e4a6cb15f | ||
|
|
73e45ce911 | ||
|
|
1e40043456 | ||
|
|
7f3b952ec7 | ||
|
|
82887dc1c1 | ||
|
|
71380ab37a | ||
|
|
5d00804758 | ||
|
|
84364c65b0 | ||
|
|
1b59b705ae | ||
|
|
bc90fe6f99 | ||
|
|
c8d6a0833e | ||
|
|
d8fc7d27ec | ||
|
|
1e44b19ff0 | ||
|
|
cc8b2cd20e | ||
|
|
057b1e294c | ||
|
|
0653e790c7 | ||
|
|
6d72bbcb76 | ||
|
|
59e6f08455 | ||
|
|
8fdccf50a8 | ||
|
|
1c7e11c5d9 | ||
|
|
1756fbbb23 | ||
|
|
7bb939ab7f | ||
|
|
4fa0abbd5a | ||
|
|
1e5f0266ef | ||
|
|
3dee62105e | ||
|
|
680b2323d5 | ||
|
|
562b4dad4d | ||
|
|
079a82dfe4 | ||
|
|
16f7eb43b0 | ||
|
|
4b0ed3f224 | ||
|
|
1d453b694d | ||
|
|
751e8c189e | ||
|
|
183e9a3d0b | ||
|
|
5f0f4dd485 | ||
|
|
20f05662aa | ||
|
|
963471b43f | ||
|
|
fdb52e0243 | ||
|
|
d1e4483f98 | ||
|
|
b194ada316 | ||
|
|
3a25a2dcbf | ||
|
|
85e4497fbe | ||
|
|
0bfa5e7ea6 | ||
|
|
013599890f | ||
|
|
519e552405 | ||
|
|
32a4ec49f7 | ||
|
|
3223332906 | ||
|
|
f78bd08440 | ||
|
|
99b5f92d7a | ||
|
|
1cc5e84104 | ||
|
|
8346f5ab08 | ||
|
|
dc60a39ba9 | ||
|
|
16f7872553 | ||
|
|
75e7c4b2ca | ||
|
|
c5d5ddd7d2 | ||
|
|
98a81e3708 | ||
|
|
ecdbdb944a | ||
|
|
8a2846461b | ||
|
|
6bfb9a2f57 | ||
|
|
85dfb2d4eb | ||
|
|
8f1d0c2b8f | ||
|
|
829494c03e | ||
|
|
f89fade28d | ||
|
|
9081b1dadd | ||
|
|
84dac544af | ||
|
|
49f1ac333d | ||
|
|
a2b761094b | ||
|
|
54bd3d480f | ||
|
|
b281f8fa32 | ||
|
|
a3732f845e | ||
|
|
38f6d0a020 | ||
|
|
a0e5da758a | ||
|
|
dcbe10c1c7 | ||
|
|
1f823d2a1b | ||
|
|
12d0a9e11a | ||
|
|
326b294c83 | ||
|
|
50f213ae71 | ||
|
|
91d4fd8849 | ||
|
|
3f1985a9dc | ||
|
|
573a71f09d | ||
|
|
57900fa287 | ||
|
|
fa721d9614 | ||
|
|
e64bbacf68 | ||
|
|
6ee2edc757 | ||
|
|
9ec4faf0d0 | ||
|
|
5be3ba2ffa | ||
|
|
a5ee96656b | ||
|
|
2db71d0323 | ||
|
|
dcf321047f | ||
|
|
4982110be9 | ||
|
|
28547e90d1 | ||
|
|
f70187597f | ||
|
|
333e454e78 | ||
|
|
61cfd11644 | ||
|
|
1c970c8176 | ||
|
|
1fea54ca5a | ||
|
|
faccc42b22 | ||
|
|
460f4769a5 | ||
|
|
f465643b75 | ||
|
|
a8afd71f96 | ||
|
|
2274d60207 | ||
|
|
57159bccfd | ||
|
|
2888124752 | ||
|
|
408e751dcf | ||
|
|
af5dcc38a4 | ||
|
|
81434640d6 | ||
|
|
a45b58d956 | ||
|
|
2c5e9a5e76 | ||
|
|
9fb847b179 | ||
|
|
b8341b2ba8 | ||
|
|
26d6e4da2c | ||
|
|
b16ed602d5 | ||
|
|
24aab4d5d3 | ||
|
|
2eb69d421a | ||
|
|
cb52706f2f | ||
|
|
f44d232e8b | ||
|
|
a0ac79b9dd | ||
|
|
177a2a8a1e | ||
|
|
8b21a87175 | ||
|
|
6d2dd8e315 | ||
|
|
8b3aff599b | ||
|
|
deb9b74f27 | ||
|
|
591de3cbe8 | ||
|
|
f7151e2132 | ||
|
|
44521be6dd | ||
|
|
30416cdbb5 | ||
|
|
d04da2d256 | ||
|
|
231d8a1949 | ||
|
|
3207bec805 | ||
|
|
425a71e382 | ||
|
|
daa12ab2ec | ||
|
|
4c652389c5 | ||
|
|
f69d88a656 | ||
|
|
098cbc1456 | ||
|
|
5f2da953a9 | ||
|
|
638d999fcc | ||
|
|
fa80fd64da | ||
|
|
e4dad82dde | ||
|
|
d65ff924c8 | ||
|
|
96e9661aaa | ||
|
|
8829d2ebd4 | ||
|
|
c019280d8a | ||
|
|
8d3e5fc160 | ||
|
|
d3f1312c0b | ||
|
|
c58f48a1e4 | ||
|
|
979e93509e | ||
|
|
135c3709b4 | ||
|
|
08400d3f18 | ||
|
|
bf52075d1b | ||
|
|
56befda288 | ||
|
|
3d68f1dc62 | ||
|
|
970036ce1a | ||
|
|
62108f28f4 | ||
|
|
66bbb072c3 | ||
|
|
6c52c26a62 | ||
|
|
d52943d1bf | ||
|
|
7cb1bbe3d6 | ||
|
|
2548c46b8b | ||
|
|
dd5118116b | ||
|
|
77a9b66028 | ||
|
|
e813dab81c | ||
|
|
6696880178 | ||
|
|
8e5952d9ae | ||
|
|
360c1d5953 | ||
|
|
a7aa6ced19 | ||
|
|
97eea669d4 | ||
|
|
c84777928e | ||
|
|
490064a953 | ||
|
|
d5975195b3 | ||
|
|
9588c36abb | ||
|
|
f9200ac135 | ||
|
|
fffb9606ec | ||
|
|
781e15cb84 | ||
|
|
9742001a71 | ||
|
|
e92eee5ffc | ||
|
|
293c1d471c | ||
|
|
384240b6a4 | ||
|
|
6fd626a3ec | ||
|
|
bb4ce2f978 | ||
|
|
2269e05058 | ||
|
|
ca2a07b816 | ||
|
|
38c7bb35e0 | ||
|
|
4f6408f3e1 | ||
|
|
7910d9fde4 | ||
|
|
0258d7e24f | ||
|
|
85556ed532 | ||
|
|
cecb04b097 | ||
|
|
8a7c5c18d0 | ||
|
|
14a1a3f574 | ||
|
|
c19f2a7499 | ||
|
|
df95be5455 | ||
|
|
00bef13f1c | ||
|
|
a6a35905a7 | ||
|
|
93f28ef55c | ||
|
|
bbcb9573cd | ||
|
|
43cc6e19d4 | ||
|
|
f4a44664c7 | ||
|
|
dd7a3269ad | ||
|
|
157d1b20c5 | ||
|
|
85fe0c00c2 | ||
|
|
91092b8a96 | ||
|
|
1ed237cfcc | ||
|
|
c7044498d5 | ||
|
|
1d2a2fbcae | ||
|
|
9b015e8cae | ||
|
|
0a8c26fff4 | ||
|
|
506de72666 | ||
|
|
a5b4156b56 | ||
|
|
da4b42cb1d | ||
|
|
53790f8247 | ||
|
|
69780d4727 | ||
|
|
aa2b644684 | ||
|
|
a12e8875a6 | ||
|
|
9e91b265ce | ||
|
|
8c12e3ab90 | ||
|
|
7e303b4fc0 | ||
|
|
40e0fcff30 | ||
|
|
3c9e74b23e | ||
|
|
6b001eb7c3 | ||
|
|
f81621aa66 | ||
|
|
08c7484087 | ||
|
|
a8c68f3e30 | ||
|
|
0e6698d760 | ||
|
|
f3d4f9ff23 | ||
|
|
d711f17081 | ||
|
|
d35eba45c5 | ||
|
|
cd53e79b19 | ||
|
|
3db7029534 | ||
|
|
ad1e52bf19 | ||
|
|
e08791a284 | ||
|
|
8d1deeb568 | ||
|
|
375c7789a2 | ||
|
|
ec8a81aedb | ||
|
|
033d513aee | ||
|
|
fb3e4e4881 | ||
|
|
8a30c006e7 | ||
|
|
3f76679673 | ||
|
|
1cee5d4b41 | ||
|
|
3107eec697 | ||
|
|
477d46316e | ||
|
|
3133693a0e | ||
|
|
bc7d701298 | ||
|
|
5d6d75b4f3 | ||
|
|
73d48a7b37 | ||
|
|
ed7b9a9989 | ||
|
|
e1a955752f | ||
|
|
0bdc8f0b2b | ||
|
|
a692b3ced8 | ||
|
|
2f5b93861d | ||
|
|
110183585c | ||
|
|
7eb29586a7 | ||
|
|
401065a23e | ||
|
|
4e5e0fb0ce | ||
|
|
d41a06611e | ||
|
|
26c3c27726 | ||
|
|
19ab63e041 | ||
|
|
5dafdab3d7 | ||
|
|
afbb17d428 | ||
|
|
8a721fbd25 | ||
|
|
5d91a409e7 | ||
|
|
8470f7caf8 | ||
|
|
67e279928e | ||
|
|
77ac3a62b7 | ||
|
|
12eaa3a162 | ||
|
|
bbd5dd7b4f | ||
|
|
38fcd6e267 | ||
|
|
fd7f683eaa | ||
|
|
e15f9acd91 | ||
|
|
7cb0882c73 | ||
|
|
486d4d1c88 | ||
|
|
ded8b13e96 | ||
|
|
c7eb7ba861 | ||
|
|
4920bff8fd | ||
|
|
d78edf5dda | ||
|
|
7510c02d83 | ||
|
|
2d7b729473 | ||
|
|
0495fe2d71 | ||
|
|
d7da5df734 | ||
|
|
4462b6bd39 | ||
|
|
80e1edeeb2 | ||
|
|
11af421f10 | ||
|
|
686ff235e7 | ||
|
|
31f76a6d4d | ||
|
|
50078078e0 | ||
|
|
be85e1e2f7 | ||
|
|
9ad1574292 | ||
|
|
4b71825707 | ||
|
|
fb1fd88947 | ||
|
|
dca527d8b6 | ||
|
|
3452a445fe | ||
|
|
a06e9d2bef | ||
|
|
7a3961a280 | ||
|
|
54729d8fb4 | ||
|
|
c2e17ee182 | ||
|
|
bc0064d4b9 | ||
|
|
03685dbb61 | ||
|
|
26fcba8ed5 | ||
|
|
bc15b65538 | ||
|
|
e9ab34a9c1 | ||
|
|
0bf512ebdd | ||
|
|
7646fbeaa0 | ||
|
|
84b4766013 | ||
|
|
3a48734b2f | ||
|
|
36ae332959 | ||
|
|
3e0d8da9d6 | ||
|
|
2fcb4dbe50 | ||
|
|
09c93bfb39 | ||
|
|
34068b1598 | ||
|
|
a67da1c99a | ||
|
|
0d6754761d | ||
|
|
898f7b66cf | ||
|
|
c18f3e86f0 | ||
|
|
de51922f10 | ||
|
|
be0cb18bfc | ||
|
|
39fd1db3c0 | ||
|
|
b4565e7354 | ||
|
|
e28cada4dd | ||
|
|
6daac65968 | ||
|
|
1ecc49c450 | ||
|
|
f96e7d9aaa | ||
|
|
c637bba867 | ||
|
|
bdc6554ca8 | ||
|
|
ecb59e9c31 | ||
|
|
1b39184e98 | ||
|
|
2a35ba64f7 | ||
|
|
3a5123627d | ||
|
|
a18eeecd59 | ||
|
|
85e3f04738 | ||
|
|
cc59864377 | ||
|
|
5b10cbf2e2 | ||
|
|
fc6b83bb5d | ||
|
|
bc509f55d9 | ||
|
|
f81301ece6 | ||
|
|
382e5c1f43 | ||
|
|
0243f74dcd | ||
|
|
58737ef454 | ||
|
|
940cea82ca | ||
|
|
5683e36733 | ||
|
|
f5137b7935 | ||
|
|
0c2af42c69 | ||
|
|
760dc5d0c6 | ||
|
|
5331aa08a7 | ||
|
|
375d7cc7b1 | ||
|
|
a05f3dd640 | ||
|
|
b91c1b44ba | ||
|
|
6efb01a397 | ||
|
|
1843eca6c0 | ||
|
|
506e3e8a48 | ||
|
|
0e5a3cc5aa | ||
|
|
d2dd76e13d | ||
|
|
470b82fd64 | ||
|
|
e04dd3a4b1 | ||
|
|
2d39e06c97 | ||
|
|
e1fc74bdc1 | ||
|
|
3ab5d7f861 | ||
|
|
d63dd6086a | ||
|
|
a8d9895a9b | ||
|
|
f8a7257af3 | ||
|
|
4703028988 | ||
|
|
87523cdbd5 | ||
|
|
d9567ed035 | ||
|
|
0ab277662a | ||
|
|
2eeb94e39c | ||
|
|
4b441d10b3 | ||
|
|
37a1d3d61e | ||
|
|
3839338c15 | ||
|
|
bdee5790e6 | ||
|
|
d0dab25dae | ||
|
|
b14b7b00c2 | ||
|
|
248bfcccda | ||
|
|
9b5833205b | ||
|
|
07f8589090 | ||
|
|
f77f83dfeb | ||
|
|
e3d3d916ba | ||
|
|
cccf219cd2 | ||
|
|
0896b2f7b8 | ||
|
|
cc406262ac | ||
|
|
0f20063eb8 | ||
|
|
5f32b165f2 | ||
|
|
3cadd1789b | ||
|
|
e486778b36 | ||
|
|
7fe6453bbb | ||
|
|
9f88d2b6d6 | ||
|
|
8f9d52699d | ||
|
|
0a774a8c55 | ||
|
|
d4ced34a11 | ||
|
|
85a762a0b9 | ||
|
|
b255fecc6e | ||
|
|
734d2e2594 | ||
|
|
2e292b4636 | ||
|
|
f0bc7356ac | ||
|
|
1bcb6ab931 | ||
|
|
ef65937662 | ||
|
|
3369b8b5b2 | ||
|
|
28db561cd9 | ||
|
|
0622326e1b | ||
|
|
c6e2593e4e | ||
|
|
d0e3279a67 | ||
|
|
aee5bda9ec | ||
|
|
979b4a8861 | ||
|
|
c10cd4b474 | ||
|
|
4aa1d19845 | ||
|
|
7ff51d89fc | ||
|
|
ea9d94e42f | ||
|
|
a9ba0fdb0b | ||
|
|
af19c3331c | ||
|
|
5e98b930ee | ||
|
|
057d160392 | ||
|
|
6b2899c219 | ||
|
|
85290e687c | ||
|
|
d778e81f42 | ||
|
|
2bfad21604 | ||
|
|
373e0d3a9f | ||
|
|
5e83403d0c | ||
|
|
cbe76aab83 | ||
|
|
26de088520 | ||
|
|
98430edb17 | ||
|
|
48c6784e51 | ||
|
|
dc0f5af3ef | ||
|
|
af85e6f2a6 | ||
|
|
4e91af4d64 | ||
|
|
faf87a5dee | ||
|
|
517c5d356f | ||
|
|
931be22247 | ||
|
|
8697360eb7 | ||
|
|
e3a867132a | ||
|
|
c96debadc5 | ||
|
|
02520d4f54 | ||
|
|
5070b63d5b | ||
|
|
eaa722b10d | ||
|
|
1bc3c90286 | ||
|
|
afd00edee3 | ||
|
|
b712398208 | ||
|
|
7586e91b4f | ||
|
|
9eba82c107 | ||
|
|
ccdc219a09 | ||
|
|
60d01e76e9 | ||
|
|
b5cfd4152e | ||
|
|
32c4c8ae32 | ||
|
|
bd4c506d22 | ||
|
|
476dd7cd56 | ||
|
|
8176f84715 | ||
|
|
6bd33721d8 | ||
|
|
c9d9671288 | ||
|
|
2a821edf5f | ||
|
|
68b85bdc87 | ||
|
|
83cf5907c3 | ||
|
|
c912b6547c | ||
|
|
bf04b74f87 | ||
|
|
9d1e008990 | ||
|
|
d9e5285a3b | ||
|
|
84937b7a0b | ||
|
|
924b3e16cf | ||
|
|
2a8cf01410 | ||
|
|
a3a5cfee6c | ||
|
|
2c04441591 | ||
|
|
a4eab8e216 | ||
|
|
189f9589d4 | ||
|
|
880721e0d0 | ||
|
|
6ab65e2031 | ||
|
|
e871934cfd | ||
|
|
686390c1f2 | ||
|
|
a8b9fb1708 | ||
|
|
55d3764169 | ||
|
|
cb5bc3d631 | ||
|
|
543e66eb00 | ||
|
|
b658983fb8 | ||
|
|
cfb3e42337 | ||
|
|
36815b5e43 | ||
|
|
897e077aca | ||
|
|
f395960ffa | ||
|
|
fb301717f5 | ||
|
|
46da93519f | ||
|
|
ce0f2c51a9 | ||
|
|
04b4b8da4f | ||
|
|
877d7451dd | ||
|
|
7e6a68a2b1 | ||
|
|
caca515ba0 | ||
|
|
d548b78dee | ||
|
|
f2410abc48 | ||
|
|
483a7d34c5 | ||
|
|
e872411285 | ||
|
|
fc7e6bf542 | ||
|
|
16d42b6421 | ||
|
|
2f25d25eec | ||
|
|
be1081a4b9 | ||
|
|
1608b652d7 | ||
|
|
5dd19a878c | ||
|
|
3314f4b5b8 | ||
|
|
5977e0fe89 | ||
|
|
f477dcba4a | ||
|
|
6c5f0c5379 | ||
|
|
257eb1bed0 | ||
|
|
9c4d142c2d | ||
|
|
8e89a1f154 | ||
|
|
b0952c0374 | ||
|
|
ac95dcb3f2 | ||
|
|
ce4043f038 | ||
|
|
0d26857e31 | ||
|
|
85bea95f6b | ||
|
|
10a46c507f | ||
|
|
d35d76e1d0 | ||
|
|
aaa05b22df | ||
|
|
c5fa30f0de | ||
|
|
43fe1a9a0e | ||
|
|
aa296fcb69 | ||
|
|
b9c7023489 | ||
|
|
efcd286039 | ||
|
|
98014f9495 | ||
|
|
2702a18ea2 | ||
|
|
4a8da3e1e2 | ||
|
|
cfe38c00f3 | ||
|
|
af0463ed46 | ||
|
|
c02f4691e0 | ||
|
|
5d89393fff | ||
|
|
e7ce28204b | ||
|
|
8fc4a75e8c | ||
|
|
26c89a09e8 | ||
|
|
25a1493520 | ||
|
|
b18722f776 | ||
|
|
3f3fd9ae21 | ||
|
|
94ea3c7dab | ||
|
|
6c2fea7926 | ||
|
|
e08fd47b0e | ||
|
|
0fd76e8768 | ||
|
|
72aaf3055a | ||
|
|
94a943a68c | ||
|
|
e867dcfdb1 | ||
|
|
9a22a89b06 | ||
|
|
791e8200bc | ||
|
|
d96217d49a | ||
|
|
6bfd65aa19 | ||
|
|
2da9bc07ac | ||
|
|
6d7a562b7a | ||
|
|
0aa1dfb8e1 | ||
|
|
e9e7dc298f | ||
|
|
ed3b71e396 | ||
|
|
3450a037a9 | ||
|
|
f57626d256 | ||
|
|
c1c3fa4d3a | ||
|
|
300433f7de | ||
|
|
eee6f4ed81 | ||
|
|
2eb29bd8aa | ||
|
|
3a0ce86f51 | ||
|
|
6041b8cbb2 | ||
|
|
3ba8fcb7b8 | ||
|
|
f74d9c93a2 | ||
|
|
739c162281 | ||
|
|
a2700c900d | ||
|
|
4e1caee7da | ||
|
|
76a54249bb | ||
|
|
0e894cb043 | ||
|
|
01bbee59eb | ||
|
|
26a0c3520c | ||
|
|
6056c35de3 | ||
|
|
4202991ca5 | ||
|
|
788931c7c7 | ||
|
|
b2d0505c7c | ||
|
|
8b710d651f | ||
|
|
93697bb01d | ||
|
|
89cd58e4f8 | ||
|
|
a622f029a0 | ||
|
|
97afb52904 | ||
|
|
02ea31be08 | ||
|
|
d1353e8eae | ||
|
|
935a76d16b | ||
|
|
db4c41f420 | ||
|
|
62f5af8e0b | ||
|
|
ff9aefb649 | ||
|
|
2b10d03e1f | ||
|
|
a27efbd937 | ||
|
|
b5e49a6619 | ||
|
|
179c931f85 | ||
|
|
4d3fa2c8ac | ||
|
|
8e4f7387d0 | ||
|
|
feb630b2c5 | ||
|
|
948dfbb56b | ||
|
|
5c3ac75b34 | ||
|
|
adc5c8e37a | ||
|
|
52d594c143 | ||
|
|
1018b0d966 | ||
|
|
0ce153d788 | ||
|
|
ff9756c739 | ||
|
|
b3dd7e5397 | ||
|
|
6ac0a80896 | ||
|
|
93f774c7e7 | ||
|
|
661c08549d | ||
|
|
c8acc44012 | ||
|
|
aabf00659e | ||
|
|
a9bc41492c | ||
|
|
12b0484e9a | ||
|
|
d3605dbcb3 | ||
|
|
d9a016f94c | ||
|
|
a13657ac23 | ||
|
|
d6c95a9e89 | ||
|
|
825e9e04c1 | ||
|
|
c596e44c5a | ||
|
|
66be9004fe | ||
|
|
119ebb0f07 | ||
|
|
d509fcac29 | ||
|
|
c79e933586 | ||
|
|
3c3cfc02a0 | ||
|
|
e32de7b940 | ||
|
|
d7b1759afb | ||
|
|
e391c1fda3 | ||
|
|
46d8d3b469 | ||
|
|
9fde7509fa | ||
|
|
a767a61f43 | ||
|
|
ad28e03536 | ||
|
|
5f9e9c2e03 | ||
|
|
2be6d7a65c | ||
|
|
712c4cb985 | ||
|
|
7948a0a4fa | ||
|
|
ce9e95f256 | ||
|
|
7ed5ca94a2 | ||
|
|
e635589c52 | ||
|
|
490721437f | ||
|
|
7e25a1566f | ||
|
|
c08b3b0c30 | ||
|
|
f1de132a2a | ||
|
|
c440c60bdf | ||
|
|
c367176a17 | ||
|
|
3d13c39a4c | ||
|
|
07f2792cf9 | ||
|
|
303fce5f15 | ||
|
|
46c8bfdd34 | ||
|
|
c36a22ad5e | ||
|
|
49eba95a9c | ||
|
|
2dacc6ce40 | ||
|
|
5ccf02f5c3 | ||
|
|
86c67de8ff | ||
|
|
dac7eb5997 | ||
|
|
fd725552a5 | ||
|
|
1e1a897970 | ||
|
|
59a643c006 | ||
|
|
1a492208e6 | ||
|
|
bc0a19f55d | ||
|
|
85e2e00bc4 | ||
|
|
93dd8bbf28 | ||
|
|
2f15a219df | ||
|
|
4fc73b1344 | ||
|
|
48b56ba08d | ||
|
|
94ca733c7c | ||
|
|
4af2436a0e | ||
|
|
20c2bb9d50 | ||
|
|
33bac0db3c | ||
|
|
8893db9098 | ||
|
|
c46658a5c8 | ||
|
|
5f651aed3e | ||
|
|
534bc9c6e2 | ||
|
|
bb09885237 | ||
|
|
3ff6aaa6db | ||
|
|
fa7c034d16 | ||
|
|
ca870ccd75 | ||
|
|
89fe2ff217 | ||
|
|
6c6775376e | ||
|
|
bd3b3881d8 | ||
|
|
078189599c | ||
|
|
9afcd2a411 | ||
|
|
0afd1649c1 | ||
|
|
0947a63103 | ||
|
|
79223bddc5 | ||
|
|
1871dd6b71 | ||
|
|
4adc3088d1 | ||
|
|
79dd7e1bf5 | ||
|
|
6718d377bb | ||
|
|
d4e3329d7a | ||
|
|
99295f0983 | ||
|
|
3606c36cb9 | ||
|
|
3d5c184acc | ||
|
|
9e03b17498 | ||
|
|
129714b044 | ||
|
|
67823556d2 | ||
|
|
273e71e3c4 | ||
|
|
076ac26929 | ||
|
|
5a022b0a2c | ||
|
|
9ab493a81f | ||
|
|
dfc1f32595 | ||
|
|
e6fd30fb78 | ||
|
|
c8d338912a | ||
|
|
d9d9e0b33f | ||
|
|
1da7b83956 | ||
|
|
29c545d2e3 | ||
|
|
431b345c82 | ||
|
|
8773b1b38f | ||
|
|
52efb3dc16 | ||
|
|
9ccd179b04 | ||
|
|
a8b35c49a7 | ||
|
|
4e027f1a45 | ||
|
|
119182454b | ||
|
|
04e10a4f0d | ||
|
|
4e5c5f9c5b | ||
|
|
1ee2a25eca | ||
|
|
838e132515 | ||
|
|
da76a843ee | ||
|
|
b0676b8b31 | ||
|
|
83c3656d29 | ||
|
|
5ddd6cc94e | ||
|
|
90419765af | ||
|
|
acad9354a6 | ||
|
|
9105dd7b04 | ||
|
|
71adee1f38 | ||
|
|
3cbe3831ec | ||
|
|
619aa4f05a | ||
|
|
4df37d6f3e | ||
|
|
58f8b482f5 | ||
|
|
168f5e32af | ||
|
|
06bf28f10c | ||
|
|
620fdc0d9f | ||
|
|
bdac0e2456 | ||
|
|
60a57a0a40 | ||
|
|
a242ae3849 | ||
|
|
ced4060b5c | ||
|
|
0b3eb7a237 | ||
|
|
c82f87cd76 | ||
|
|
51d8a6d9bf | ||
|
|
d334aa2088 | ||
|
|
710e003bdc | ||
|
|
b2f5b4f861 | ||
|
|
0ac87930c8 | ||
|
|
241a482236 | ||
|
|
2abaffafcf | ||
|
|
4545fedf31 | ||
|
|
a47a690a68 | ||
|
|
f89c44e899 | ||
|
|
59b0df5c82 | ||
|
|
5ec6ffb30a | ||
|
|
5956d2009c | ||
|
|
d9c7f21c02 | ||
|
|
926e508b8d | ||
|
|
ac83772945 | ||
|
|
cddf5cf70f | ||
|
|
d53acdb46a | ||
|
|
cfae8f4fc6 | ||
|
|
74cd4cecbf | ||
|
|
3e9e6a1389 | ||
|
|
9788450c08 | ||
|
|
10b27aed34 | ||
|
|
64f95be828 | ||
|
|
a54634023b | ||
|
|
9d942b78ef | ||
|
|
4cd5357241 | ||
|
|
f985a96988 | ||
|
|
0e3938da79 | ||
|
|
ec9bfc4731 | ||
|
|
9b91ebb8d2 | ||
|
|
da3f2367d7 | ||
|
|
17cdeec34b | ||
|
|
3446afd087 | ||
|
|
b12fef652c | ||
|
|
21c7193281 | ||
|
|
a5e64274a2 | ||
|
|
3817202875 | ||
|
|
874fcb12a1 | ||
|
|
e0c5783703 | ||
|
|
a57e037b05 | ||
|
|
8546918cbb | ||
|
|
82284029f2 | ||
|
|
7c20e865a5 | ||
|
|
79267d4e12 | ||
|
|
50aeb70597 | ||
|
|
1d22a79074 | ||
|
|
7f442f4206 | ||
|
|
985326989c | ||
|
|
be8f2afa37 | ||
|
|
98882984b4 | ||
|
|
a6cd0fdb85 | ||
|
|
7bc5ba7e9a | ||
|
|
37e552cd36 | ||
|
|
51e2a4de7d | ||
|
|
91ce2fcb06 | ||
|
|
925a379702 | ||
|
|
3153cfd0ff | ||
|
|
ac8831b4c7 | ||
|
|
acc535e1a4 | ||
|
|
fdacb4fe7d | ||
|
|
fc7208469d | ||
|
|
5c38cb733a | ||
|
|
515a67a320 | ||
|
|
941348f1db | ||
|
|
8d7752b0bc | ||
|
|
15af660424 | ||
|
|
790555ae89 | ||
|
|
3cc4df4e29 | ||
|
|
395d1cee70 | ||
|
|
89bc7efbca | ||
|
|
8f893a9752 | ||
|
|
54e02e412c | ||
|
|
808d7aab3f | ||
|
|
24a8f8f38b | ||
|
|
90c00bed2f | ||
|
|
054c911a1f | ||
|
|
c2d5432a5d | ||
|
|
dd64b70f5b | ||
|
|
a69dbeb10f | ||
|
|
976a768446 | ||
|
|
5612cec91f | ||
|
|
46996eb81c | ||
|
|
12990f9fb2 | ||
|
|
77ff988232 | ||
|
|
8df98c29b8 | ||
|
|
7554f8395b | ||
|
|
6fc0bac106 | ||
|
|
d252feddc9 | ||
|
|
8c2645498d | ||
|
|
528acc4aa4 | ||
|
|
2ce45eab06 | ||
|
|
074c24aa10 | ||
|
|
79e4007732 | ||
|
|
742ce6673c | ||
|
|
4a74f588b9 | ||
|
|
f876dc066c | ||
|
|
65aaf386c2 | ||
|
|
4eae6bd362 | ||
|
|
4f7cfd6bd4 | ||
|
|
1136ad09ee | ||
|
|
4a67bd945c | ||
|
|
a2841f7cf2 | ||
|
|
4fa6e9dafe | ||
|
|
7ebd7959f9 | ||
|
|
4386015cff | ||
|
|
5cd014f7e6 | ||
|
|
30d0bfbdf0 | ||
|
|
48cfeca220 | ||
|
|
5fefc48a0b | ||
|
|
42f2ae16ec | ||
|
|
3d47b0201f | ||
|
|
7607b1215f | ||
|
|
eae2b40898 | ||
|
|
8e253046d8 | ||
|
|
b0e17f02b5 | ||
|
|
3f3c131737 | ||
|
|
2b227fcca5 | ||
|
|
639e55b537 | ||
|
|
8386404b25 | ||
|
|
a093afb630 | ||
|
|
7208688128 | ||
|
|
b94d406bd9 | ||
|
|
79188b7d62 | ||
|
|
85b2fc503d | ||
|
|
57f33109b2 | ||
|
|
d039547886 | ||
|
|
55621b7826 | ||
|
|
854730f258 | ||
|
|
9c4f73f314 | ||
|
|
e1fa491af7 | ||
|
|
b909a3e05c | ||
|
|
4159c539e7 | ||
|
|
d1bd232ec7 | ||
|
|
a47898a2c4 | ||
|
|
06b69e516a | ||
|
|
312c89aaee | ||
|
|
a5b1f020ae | ||
|
|
e9cd1bef43 | ||
|
|
7b17ce5de1 | ||
|
|
ffb1b06bf4 | ||
|
|
21e9ffec97 | ||
|
|
7cfaa2adfc | ||
|
|
4e80e35976 | ||
|
|
e9f78f6ace | ||
|
|
4d074c6fa9 | ||
|
|
0b3c2e198e | ||
|
|
48cd7200cc | ||
|
|
d925702b98 | ||
|
|
55cbd72a47 | ||
|
|
fbf1ba172f | ||
|
|
abb722f405 | ||
|
|
2c121dc2c3 | ||
|
|
dbcdbda2ef | ||
|
|
03784ba82e | ||
|
|
04768ad8fa | ||
|
|
29943408e5 | ||
|
|
dbadc05bef | ||
|
|
2e6347b380 | ||
|
|
6c65b1ffde | ||
|
|
358416687f | ||
|
|
1dc061e1c6 | ||
|
|
e7f2572f42 | ||
|
|
463ee7e027 | ||
|
|
667bc95d02 | ||
|
|
3f8cc71814 | ||
|
|
a9a6e96a6a | ||
|
|
02caf05aaf | ||
|
|
da88678a43 | ||
|
|
1e8a9de60a | ||
|
|
1a6181cb15 | ||
|
|
ece69342b8 | ||
|
|
fe601d631b | ||
|
|
42d2e5aaef | ||
|
|
bac7ab7f01 | ||
|
|
c6ac1474d4 | ||
|
|
5a9c4ad8f3 | ||
|
|
babc59f85c | ||
|
|
982a4361cd | ||
|
|
15e53ca55e | ||
|
|
ceb428b8bd | ||
|
|
b7fe3ed745 | ||
|
|
817b614e61 | ||
|
|
3321e95b54 | ||
|
|
fd5f536d14 | ||
|
|
fdbcb32be7 | ||
|
|
a774cee1d9 | ||
|
|
1c46f227d2 | ||
|
|
6191c96347 | ||
|
|
12918fdfee | ||
|
|
8cea246fc8 | ||
|
|
41455a008b | ||
|
|
be22d5c95d | ||
|
|
b298a62e46 | ||
|
|
839045ed66 | ||
|
|
a4a5c61fef | ||
|
|
dd6aa9e4cf | ||
|
|
e299b51779 | ||
|
|
d3f0129c3f | ||
|
|
ae938c66cc | ||
|
|
8d662f4f89 | ||
|
|
a1caa41fe9 | ||
|
|
6028263681 | ||
|
|
30f9291496 | ||
|
|
2400a44ded | ||
|
|
952556a9d1 | ||
|
|
2e18b5c64b | ||
|
|
cb499c6105 | ||
|
|
7f87bcddc1 | ||
|
|
6798917bc9 | ||
|
|
cf3f32ac37 | ||
|
|
a41e1dafc2 | ||
|
|
3ac5124e5c | ||
|
|
83a472794b | ||
|
|
af78d62b76 | ||
|
|
27a0708909 | ||
|
|
5ab58f86ba | ||
|
|
2c0bfdccf9 | ||
|
|
581618370b | ||
|
|
bdf217c45a | ||
|
|
42d6594159 | ||
|
|
fac1750b63 | ||
|
|
6907d192f5 | ||
|
|
0380f36489 | ||
|
|
1524dc2680 | ||
|
|
c95cc0a52b | ||
|
|
a876c5a888 | ||
|
|
bdd925886a | ||
|
|
4d3d292add | ||
|
|
48f03e79e2 | ||
|
|
e3af622a36 | ||
|
|
dcb300ab50 | ||
|
|
95ffd46a63 | ||
|
|
6f5d4a8620 | ||
|
|
62855cc969 | ||
|
|
bdfe9dfab2 | ||
|
|
465ddf1a01 | ||
|
|
18a4ac1653 | ||
|
|
76d55144c5 | ||
|
|
665590a2f9 | ||
|
|
81aa9a31c6 | ||
|
|
082490708f | ||
|
|
20ca4f8260 | ||
|
|
eda057eb07 | ||
|
|
aed31c0eba | ||
|
|
ea585458c7 | ||
|
|
e226e1c045 | ||
|
|
ef330dd613 | ||
|
|
1225560ccd | ||
|
|
eef184e6ef | ||
|
|
50337eb731 | ||
|
|
77f5e7d581 | ||
|
|
02118bac76 | ||
|
|
8b41be238a | ||
|
|
8791e70c67 | ||
|
|
34a4b8f2d2 | ||
|
|
54c936b010 | ||
|
|
ed8d95e055 | ||
|
|
25e8d080af | ||
|
|
4599a7ab4e | ||
|
|
5a106e0e6d | ||
|
|
e4a211ba02 | ||
|
|
7ab373c942 | ||
|
|
18f1ebf715 | ||
|
|
2c02f44b26 | ||
|
|
8ae8168252 | ||
|
|
c504f0b166 | ||
|
|
23da559d26 | ||
|
|
8b3ca12658 | ||
|
|
0f70b5662c | ||
|
|
475541cac0 | ||
|
|
007d6ad9c3 | ||
|
|
79db8d91ab | ||
|
|
a0c6feb386 | ||
|
|
1c72601123 | ||
|
|
c6bcdd1ae1 | ||
|
|
e0acdba626 | ||
|
|
a73e72f267 | ||
|
|
ee6682e7c8 | ||
|
|
6c36ea5753 | ||
|
|
3f798f1843 | ||
|
|
4a806db8ae | ||
|
|
9c12125512 | ||
|
|
51130793fe | ||
|
|
d39c93ee7e | ||
|
|
e1cfb29fa0 | ||
|
|
4dde16d836 | ||
|
|
1c57ef5931 | ||
|
|
0f642fe3ad | ||
|
|
46844b516a | ||
|
|
b9cef228d9 | ||
|
|
fd71d04000 | ||
|
|
bedb811621 | ||
|
|
173736fd69 | ||
|
|
21eea27c4a | ||
|
|
b39f04fbc3 | ||
|
|
04f898be21 | ||
|
|
defedda891 | ||
|
|
bb2c8e5fd2 | ||
|
|
7410bb9e9a | ||
|
|
fd3907596b | ||
|
|
221a233ea4 | ||
|
|
389adda8ec | ||
|
|
835b65740b | ||
|
|
b24abffd0d | ||
|
|
2ddf578f07 | ||
|
|
61b83c3e34 | ||
|
|
3454ddbb2a | ||
|
|
194e4ca70d | ||
|
|
074981a709 | ||
|
|
f157200a9f | ||
|
|
f70fcddaad | ||
|
|
aa2129b60e | ||
|
|
3794541828 | ||
|
|
b1a6dc65ba | ||
|
|
040ae293fb | ||
|
|
b74e93f3a4 | ||
|
|
dd3cc29af1 | ||
|
|
72b9a93088 | ||
|
|
4446f0a1f3 | ||
|
|
35802c12f6 | ||
|
|
f5fd30883a | ||
|
|
f8e720d6dc | ||
|
|
9f8f04caec | ||
|
|
69aec85882 | ||
|
|
e84012ec38 | ||
|
|
128df6a2bf | ||
|
|
7b76951461 | ||
|
|
38d87450e6 | ||
|
|
b9c6b84ff5 | ||
|
|
5ba1d57b1f | ||
|
|
a4d567e44d | ||
|
|
19a62ba7e9 | ||
|
|
65d6c7b558 | ||
|
|
8c828e7881 | ||
|
|
b711780688 | ||
|
|
855cdc8eda | ||
|
|
c930cce914 | ||
|
|
424696cf96 | ||
|
|
ea4ef45289 | ||
|
|
d2e13e6ac6 | ||
|
|
70ff6658eb | ||
|
|
3e1732b9c0 | ||
|
|
4eb18dc797 | ||
|
|
6e7adfadb0 | ||
|
|
a866224f7b | ||
|
|
77c50d6880 | ||
|
|
4290e2e7b0 | ||
|
|
4e8632de73 | ||
|
|
2792fe82a3 | ||
|
|
ab6df9f8a9 | ||
|
|
425489c74b | ||
|
|
9f04517801 | ||
|
|
9954639096 | ||
|
|
cd78364c17 | ||
|
|
563b986591 | ||
|
|
fe5552773d | ||
|
|
b7acaf9519 | ||
|
|
deffc90531 | ||
|
|
f54a6e8513 | ||
|
|
ab4dc641af | ||
|
|
dfe725dd78 | ||
|
|
dbe1e24561 | ||
|
|
075557929a | ||
|
|
d994db9c74 | ||
|
|
c462c61ac9 | ||
|
|
1ef9719059 | ||
|
|
3df0e7899c | ||
|
|
927972d889 | ||
|
|
00f409d21d | ||
|
|
6aea95b486 | ||
|
|
e4dd78b9a4 | ||
|
|
d940d5a597 | ||
|
|
5f1ea21be1 | ||
|
|
278851224d | ||
|
|
0161bc5a4e | ||
|
|
9cf5168d3f | ||
|
|
f066a4bdf4 | ||
|
|
0009756ed9 | ||
|
|
481e5437f1 | ||
|
|
238f54e011 | ||
|
|
180525f538 | ||
|
|
9a1958d19a | ||
|
|
e1469a1e44 | ||
|
|
85bde2b00d | ||
|
|
b20a15b4ed | ||
|
|
a67161f521 | ||
|
|
8d621e87a0 | ||
|
|
a560034614 | ||
|
|
b329c1f94c | ||
|
|
06459c0f52 | ||
|
|
2655c2511d | ||
|
|
fa9fdb01b0 | ||
|
|
4896948d38 | ||
|
|
133a37817e | ||
|
|
0cdee5ed7a | ||
|
|
2c21989b47 | ||
|
|
17f796847d | ||
|
|
02578dbf3a | ||
|
|
9f86e84830 | ||
|
|
1ae65bb999 | ||
|
|
ca72f5fa99 | ||
|
|
bc9b537d84 | ||
|
|
d9336d0ba2 | ||
|
|
eb537eaa24 | ||
|
|
f1d814ba04 | ||
|
|
760d360e96 | ||
|
|
5431fa1405 | ||
|
|
eb809e388b | ||
|
|
0205ad7a4f | ||
|
|
9599b14aa8 | ||
|
|
d72585e3d1 | ||
|
|
735a15d7c6 | ||
|
|
0984e6ed90 | ||
|
|
6004a4250a | ||
|
|
b39fcc75d0 | ||
|
|
a401553c13 | ||
|
|
a312abff31 | ||
|
|
92c1722469 | ||
|
|
e48136d31e | ||
|
|
b9d6f6fd9c | ||
|
|
66c8dfe44d | ||
|
|
5910c936b9 | ||
|
|
a2645061fe | ||
|
|
7b887d2fd5 | ||
|
|
8d2367ed82 | ||
|
|
075583f4be | ||
|
|
ce86e0a54a | ||
|
|
9308f8ea69 | ||
|
|
cc8b425adc | ||
|
|
25cf7955c9 | ||
|
|
d393865efc | ||
|
|
9d877320d8 | ||
|
|
1f72bef393 | ||
|
|
99c1069229 | ||
|
|
28718429e3 | ||
|
|
e145021760 | ||
|
|
b7dd51c594 | ||
|
|
9ebc3e5eec | ||
|
|
4db60d7b27 | ||
|
|
5d85199ae3 | ||
|
|
088c9e5450 | ||
|
|
bd51d1b2a2 | ||
|
|
0de7b6f63a | ||
|
|
85706eac84 | ||
|
|
96b646b94a | ||
|
|
753f6fc671 | ||
|
|
3f186747ed | ||
|
|
e6748c0793 | ||
|
|
2383796c49 | ||
|
|
fb38ab4ef2 | ||
|
|
1242a62bca | ||
|
|
55ceb12277 | ||
|
|
6dd4e7676d | ||
|
|
4abb58b239 | ||
|
|
385dab6036 | ||
|
|
2e9291decf | ||
|
|
623036f03b | ||
|
|
98075e9d9d | ||
|
|
b80ce0e222 | ||
|
|
b0162efb1f | ||
|
|
7b3e9ab031 | ||
|
|
b35e866d95 | ||
|
|
2d273d1550 | ||
|
|
6aef3eb879 | ||
|
|
61b0393657 | ||
|
|
30bf6c0714 | ||
|
|
fc64b115d3 | ||
|
|
bdc3f24ca6 | ||
|
|
b28300b184 | ||
|
|
5c1f9d03e4 | ||
|
|
ed368a225d | ||
|
|
2f17f392d5 | ||
|
|
b00a6e526e | ||
|
|
75031df45b | ||
|
|
39148a99d0 | ||
|
|
d72f5ee2ad | ||
|
|
c6ea459a68 | ||
|
|
e8525e5513 | ||
|
|
9046b85ca4 | ||
|
|
19257b76d2 | ||
|
|
fd8990b294 | ||
|
|
eb34f5e058 | ||
|
|
f8ced7e065 | ||
|
|
bf0a2d3c6c | ||
|
|
f5878326f7 | ||
|
|
1f0b439168 | ||
|
|
3464e11152 | ||
|
|
b5f99be635 | ||
|
|
3153b35c2a | ||
|
|
c450f9abc2 | ||
|
|
912434b259 | ||
|
|
5f61fc658a | ||
|
|
a268b74636 | ||
|
|
82a1bf7afd | ||
|
|
4392676667 | ||
|
|
c47ec2c3c0 | ||
|
|
e756fc8daa | ||
|
|
4629262a1a | ||
|
|
741aec198b | ||
|
|
c3e5b4fa36 | ||
|
|
d022907bcd | ||
|
|
8a7603a45c | ||
|
|
2ade82d883 | ||
|
|
3d8c38b558 | ||
|
|
0f2d0a33b4 | ||
|
|
d4ce1499cc | ||
|
|
9ab7895772 | ||
|
|
ee9f79e287 | ||
|
|
ad61dafb9a | ||
|
|
b81361ff6d | ||
|
|
52417c0a60 | ||
|
|
000beeb737 | ||
|
|
7cce94000b | ||
|
|
7a4bdb4191 | ||
|
|
279a8ff989 | ||
|
|
ed06a05ff0 | ||
|
|
a56ac24cc2 | ||
|
|
c8efd31d08 | ||
|
|
d8875151e8 | ||
|
|
6082431161 | ||
|
|
846c23a5a2 | ||
|
|
9a5faa92c4 | ||
|
|
f149be26ce | ||
|
|
c6d6ca5a5c | ||
|
|
98dc8069e3 | ||
|
|
32f5d68e51 | ||
|
|
855347a5d3 | ||
|
|
78c22cb74a | ||
|
|
8626a3efea | ||
|
|
d7e0d64ac2 | ||
|
|
e26b1e54c8 | ||
|
|
594024472d | ||
|
|
5f60d09d0b | ||
|
|
a05abda25c | ||
|
|
daa8e44180 | ||
|
|
837a26daee | ||
|
|
1223d3a2b3 | ||
|
|
a4d7eaf709 | ||
|
|
4b12d5527b | ||
|
|
e7553d6f15 | ||
|
|
b795d7de4e | ||
|
|
e732afd64c | ||
|
|
40b25e3826 | ||
|
|
b4d80b467a | ||
|
|
62ef377dd1 | ||
|
|
b85b1e95b4 | ||
|
|
f2ab7001d2 | ||
|
|
7fa6a2e9ef | ||
|
|
c499bbaa9f | ||
|
|
f2b16c414f | ||
|
|
2a73e28b19 | ||
|
|
9252d1290c | ||
|
|
049bd85b29 | ||
|
|
f95a0fa1d3 | ||
|
|
416466a0e1 | ||
|
|
bfd97c5484 | ||
|
|
9c1badd6aa | ||
|
|
929cb29c5b | ||
|
|
1e788962ef | ||
|
|
26f8c11526 | ||
|
|
a9b3922061 | ||
|
|
00ebe4fab2 | ||
|
|
1bee6aece7 | ||
|
|
be4eff413d | ||
|
|
e56571840b | ||
|
|
f931222256 | ||
|
|
25ff7d430d | ||
|
|
d9cf3ceb9b | ||
|
|
c37a84ab4d | ||
|
|
cf97fc9b95 | ||
|
|
03a8651cfd | ||
|
|
b206525f8e | ||
|
|
443850b778 | ||
|
|
6962fbf6f7 | ||
|
|
a2a7173e30 | ||
|
|
698ad2b890 | ||
|
|
dd43a82042 | ||
|
|
a25515dc2b | ||
|
|
e1a8e119bf | ||
|
|
4712b72019 | ||
|
|
2d162b0d4e | ||
|
|
1ea4b4d0b5 | ||
|
|
c75c7019a8 | ||
|
|
67ab5fbc8a | ||
|
|
31a5bee228 | ||
|
|
bcd7195bf5 | ||
|
|
322a9d397a | ||
|
|
71e6646ea0 | ||
|
|
b505507c35 | ||
|
|
76535c1da5 | ||
|
|
9edd94e1c0 | ||
|
|
32449b3c55 | ||
|
|
76c3d38e11 | ||
|
|
0e8ed7d770 | ||
|
|
5af4743f2f | ||
|
|
ab86232294 | ||
|
|
0202daf551 | ||
|
|
3b062de156 | ||
|
|
65786c4d41 | ||
|
|
d3bb742308 | ||
|
|
f98aa3f12b | ||
|
|
70881bb367 | ||
|
|
50adb5fdf5 | ||
|
|
c05701fc19 | ||
|
|
5e68b5f3e3 | ||
|
|
5f46d97409 | ||
|
|
7f07e47488 | ||
|
|
3999f4d3a5 | ||
|
|
b3d6220b01 | ||
|
|
7d88775b5a | ||
|
|
2ca5d3e0df | ||
|
|
3f34030a12 | ||
|
|
11dfa3d9aa | ||
|
|
6923a11038 | ||
|
|
33d78fcf29 | ||
|
|
2b3cfdf18b | ||
|
|
aa7a3f7013 | ||
|
|
1e1b3c8f5f | ||
|
|
a7cac36974 | ||
|
|
3af800e522 | ||
|
|
cf135dd658 | ||
|
|
4e9d113896 | ||
|
|
af8e81d208 | ||
|
|
83fa1ea4a6 | ||
|
|
a72f8b7b65 | ||
|
|
1b940a3117 | ||
|
|
5012831e7b | ||
|
|
85fb08e9a5 | ||
|
|
7ac84b6a91 | ||
|
|
3a72526016 | ||
|
|
c3e78f41b0 | ||
|
|
fdea9dddee | ||
|
|
2d3dd7d91d | ||
|
|
2487245e94 | ||
|
|
a4f12691ce | ||
|
|
e7e83eb8bb | ||
|
|
e8ffb68c08 | ||
|
|
283f69cb65 | ||
|
|
0fb4ab2dcf | ||
|
|
063ac989be | ||
|
|
82fb11a7b5 | ||
|
|
d238d61906 | ||
|
|
22fdc90159 | ||
|
|
b6cf4c4375 | ||
|
|
8aa32fff34 | ||
|
|
76d6ffea4a | ||
|
|
ea8ca8ea1e | ||
|
|
ed9ca04c58 | ||
|
|
093fbaa178 | ||
|
|
5f4c8cf176 | ||
|
|
c7ee37804c | ||
|
|
e0c31aa5af | ||
|
|
ab1494777d | ||
|
|
c01fafb605 | ||
|
|
5be2ffc0b2 | ||
|
|
3ede9396da | ||
|
|
e400a7a15e | ||
|
|
2939006a9a | ||
|
|
df975a0b6b | ||
|
|
bda86de632 | ||
|
|
5738a4ba48 | ||
|
|
38cfa46131 | ||
|
|
9525df1381 | ||
|
|
d393577ba8 | ||
|
|
f7c2a07b70 | ||
|
|
adf69b4890 | ||
|
|
593d22b640 | ||
|
|
7706eebafb | ||
|
|
91bd5127fb | ||
|
|
b87a0d7f25 | ||
|
|
0f431ed384 | ||
|
|
d520921b13 | ||
|
|
8d4b9076f7 | ||
|
|
93ebbcaf04 | ||
|
|
ecde1580fd | ||
|
|
8f73bb222c | ||
|
|
1108e04eff | ||
|
|
0f1a8f3358 | ||
|
|
77de4df0ff | ||
|
|
0564de37ee | ||
|
|
8c584ae0e0 | ||
|
|
30c80279c8 | ||
|
|
3f0ab9a88a | ||
|
|
6da3fcf446 | ||
|
|
26746ca303 | ||
|
|
64158eac43 | ||
|
|
9e3a7f5eda | ||
|
|
0a4f3db8ae | ||
|
|
c095085d84 | ||
|
|
41cc4e68e3 | ||
|
|
21fb7959a5 | ||
|
|
3aa22127e0 | ||
|
|
a679a37ffa | ||
|
|
649857ec28 | ||
|
|
4a6136b918 | ||
|
|
6e1c468a80 | ||
|
|
cadcd0c5e8 | ||
|
|
a531c306f1 | ||
|
|
b0a9449335 | ||
|
|
b6deb87cae | ||
|
|
7319c88674 | ||
|
|
d36317c563 | ||
|
|
1917202bd0 | ||
|
|
a8338912e0 | ||
|
|
bfea3201e8 | ||
|
|
a9bb440e6c | ||
|
|
f0942d58e8 | ||
|
|
aa093659a0 | ||
|
|
cccdbc05a5 | ||
|
|
dfa32b7be4 | ||
|
|
c7e0bd037a | ||
|
|
6c711b76b0 | ||
|
|
9c914f10c4 | ||
|
|
eda56d118a | ||
|
|
02c7351c6d | ||
|
|
ab618235f1 | ||
|
|
e4239c924b | ||
|
|
ffead9ed70 | ||
|
|
36aefadced | ||
|
|
75ccfe38ce | ||
|
|
e3cb3fe2e4 | ||
|
|
983e7e9b75 | ||
|
|
3db47c076c | ||
|
|
eeff285b33 | ||
|
|
029595f8ea | ||
|
|
ea2ec27724 | ||
|
|
f6bf4a416f | ||
|
|
af978a68e3 | ||
|
|
89dc1323e1 | ||
|
|
a4b5f63deb | ||
|
|
feaa6ccff4 | ||
|
|
7159293337 | ||
|
|
4a5b31e3a7 | ||
|
|
6f1dc89fb3 | ||
|
|
29dd405fe5 | ||
|
|
0f0b0cd3d8 | ||
|
|
262528e36a | ||
|
|
597e86dd57 | ||
|
|
b604dba948 | ||
|
|
1837a64bd2 | ||
|
|
9b413de4d8 | ||
|
|
3d77cbd677 | ||
|
|
62176c3218 | ||
|
|
d7a01c32cc | ||
|
|
cc493fd545 | ||
|
|
6b8679454d | ||
|
|
1b68d61e54 | ||
|
|
418de862e6 | ||
|
|
413653858e | ||
|
|
f0886a7556 | ||
|
|
0e2666948f | ||
|
|
d2fc851816 | ||
|
|
e1fb29c8c5 | ||
|
|
fe3158fdd6 | ||
|
|
721b26f80b | ||
|
|
d3ecfb22ee | ||
|
|
27a98020c9 | ||
|
|
ab56b72f39 | ||
|
|
8063f66958 | ||
|
|
bf270b9adb | ||
|
|
972db08740 | ||
|
|
6326c7cbaa | ||
|
|
4152ace514 | ||
|
|
038221408c | ||
|
|
9f76def7ce | ||
|
|
1b83770c5c | ||
|
|
3458d924ca | ||
|
|
0749b9500c | ||
|
|
b1dfc18a8c | ||
|
|
7b25c282c0 | ||
|
|
a128ceaf2d | ||
|
|
f266cab580 | ||
|
|
23bf9aaf17 | ||
|
|
1983f60ec6 | ||
|
|
27f8909406 | ||
|
|
9988206911 | ||
|
|
31fe1fdfa6 | ||
|
|
77b125ce2d | ||
|
|
6e68e07aa2 | ||
|
|
86bb010a93 | ||
|
|
4a623b596b | ||
|
|
bcf098ea7d | ||
|
|
4bfb226fb5 | ||
|
|
691615108b | ||
|
|
858ab00e36 | ||
|
|
7023f5b145 | ||
|
|
eb4fabeac6 | ||
|
|
a5e09f9ce4 | ||
|
|
c2fe4e8b6d | ||
|
|
5d22648d34 | ||
|
|
066fd15184 | ||
|
|
fe90c230d5 | ||
|
|
0b5ae92136 | ||
|
|
1c5565aaee | ||
|
|
69c177a3ec | ||
|
|
0645b3f65b | ||
|
|
9e7471fcc0 | ||
|
|
c520f53799 | ||
|
|
0bf1386802 | ||
|
|
b2ab3797aa | ||
|
|
ede0ca8bd1 | ||
|
|
81e35f0cc3 | ||
|
|
237522a1f7 | ||
|
|
2f94e1d2c9 | ||
|
|
2689dd32bb | ||
|
|
ad5e703b8f | ||
|
|
d3bc2e9279 | ||
|
|
0cd1644cf1 | ||
|
|
f02b405c12 | ||
|
|
baa7036799 | ||
|
|
431aecaf00 | ||
|
|
f31bb56ea6 | ||
|
|
cf3b805c46 | ||
|
|
517283ca58 | ||
|
|
f416b7ba47 | ||
|
|
973190b7a1 | ||
|
|
f536a9d3d3 | ||
|
|
1348ec3bcf | ||
|
|
9a250a4861 | ||
|
|
6450927192 | ||
|
|
7c9dbdc802 | ||
|
|
8d460afe2d | ||
|
|
6c44c2cf24 | ||
|
|
cea550ebba | ||
|
|
911a352ee6 | ||
|
|
3fadfbe06e | ||
|
|
5bf362927f | ||
|
|
4da5ca5ba9 | ||
|
|
d747005b30 | ||
|
|
03a395107d | ||
|
|
58ef4ccabf | ||
|
|
71ed082bb5 | ||
|
|
0819ac8124 | ||
|
|
0cdd223172 | ||
|
|
571393f146 | ||
|
|
c85868c652 | ||
|
|
a7a6f3b020 | ||
|
|
3a0a11d55a | ||
|
|
7eb8ddf372 | ||
|
|
87af63644a | ||
|
|
0a9dd18070 | ||
|
|
f82b061ba7 | ||
|
|
c17509e2a0 | ||
|
|
cb383d4f62 | ||
|
|
451f950d0d | ||
|
|
bd0eae0961 | ||
|
|
53a401f847 | ||
|
|
b288f5ca19 | ||
|
|
7a2fc19c4f | ||
|
|
046d712d6a | ||
|
|
e829aaecf1 | ||
|
|
9ab2f5338e | ||
|
|
d7bda924be | ||
|
|
07eb2e51b7 | ||
|
|
dfafa7ae40 | ||
|
|
dde266768c | ||
|
|
01c81675f7 | ||
|
|
71972eb362 | ||
|
|
eb9f5f9025 | ||
|
|
eb4d4d7437 | ||
|
|
1cb5e09109 | ||
|
|
cc82fff5d3 | ||
|
|
3212e59dcc | ||
|
|
44a795bf18 | ||
|
|
376e6f35a2 | ||
|
|
3b324e9532 | ||
|
|
a0df8f3490 | ||
|
|
6c14789362 | ||
|
|
880a12b914 | ||
|
|
93d69400e6 | ||
|
|
d4829e49ea | ||
|
|
1c56be3a6b | ||
|
|
07a0dfddc7 | ||
|
|
b86f9086ef | ||
|
|
343ca12c6f | ||
|
|
af3c4f84b6 | ||
|
|
3679e8795f | ||
|
|
39b4805a76 | ||
|
|
3bdcdf96d4 | ||
|
|
b54e5d33bc | ||
|
|
85e020a513 | ||
|
|
5b6268f5bc | ||
|
|
063b58eebb | ||
|
|
f8b38e4683 | ||
|
|
18e85c32b4 | ||
|
|
831fba9a53 | ||
|
|
b1f233cd8c | ||
|
|
3d0caba695 | ||
|
|
79c92f1f8e | ||
|
|
87f26e47bb | ||
|
|
9d8d04ae28 | ||
|
|
a42f046ff8 | ||
|
|
01c24a578b | ||
|
|
6b82354129 | ||
|
|
b00fbda1ae | ||
|
|
bab200ff03 | ||
|
|
357e81aeca | ||
|
|
3189c748b5 | ||
|
|
2700643cbf | ||
|
|
b0f95cd9e0 | ||
|
|
ff628ac0b2 | ||
|
|
f21aefe9e9 | ||
|
|
8babbd69d8 | ||
|
|
11bf02eb56 | ||
|
|
f5fa7d6d4b | ||
|
|
77bff6e6c2 | ||
|
|
e84a76cebd | ||
|
|
342265be94 | ||
|
|
62ec9291d8 | ||
|
|
dee6fbcb8f | ||
|
|
72fa9a2dcb | ||
|
|
ca27a9e31a | ||
|
|
18d0f45cf9 | ||
|
|
424fd515a4 | ||
|
|
00b40d64a1 | ||
|
|
3a7d0a5a9f | ||
|
|
57a02318e3 | ||
|
|
8f6d8cf0d6 | ||
|
|
5b6605b296 | ||
|
|
4d83596595 | ||
|
|
7e12a281f5 | ||
|
|
c63c10e48a | ||
|
|
155554f0b7 | ||
|
|
26b0836756 | ||
|
|
a87dc9bab2 | ||
|
|
9c1555a110 | ||
|
|
fbda2db884 | ||
|
|
2a229774ef | ||
|
|
137e5b13ef | ||
|
|
7920d66cd0 | ||
|
|
9f2dae7f3b | ||
|
|
ffde0ad1f5 | ||
|
|
2c2658a8ec | ||
|
|
6f2f8f6f7a | ||
|
|
4b6dcdd1b0 | ||
|
|
de346fd6c3 | ||
|
|
bf9d7c2012 | ||
|
|
143803f86d | ||
|
|
311143451d | ||
|
|
c9030f401d | ||
|
|
8668ddce74 | ||
|
|
7a495357f7 | ||
|
|
13864a811d | ||
|
|
5b65e4b250 | ||
|
|
dfe4a80501 | ||
|
|
bf82b9742a | ||
|
|
829a466f72 | ||
|
|
1206c70c42 | ||
|
|
3c32c349b9 | ||
|
|
0709f08d65 | ||
|
|
50f78c6e40 | ||
|
|
7e7afc6d38 | ||
|
|
1130eadac8 | ||
|
|
959fc2bbb2 | ||
|
|
f8ae505011 | ||
|
|
cd183a1926 | ||
|
|
bb2796fbc3 | ||
|
|
5de7103890 | ||
|
|
a78c91ba7e | ||
|
|
fca50da57b | ||
|
|
61f2c908b1 | ||
|
|
4c096ac068 | ||
|
|
2c95678be1 | ||
|
|
1a643cecf3 | ||
|
|
aa10b2e8c4 | ||
|
|
0b9317d047 | ||
|
|
4d58f05f38 | ||
|
|
6e879c8156 | ||
|
|
b6ee67aa41 | ||
|
|
07bed0c7c7 | ||
|
|
d2bd59d149 | ||
|
|
7bdac5a44e | ||
|
|
51f5db4374 | ||
|
|
e395ae6555 | ||
|
|
1df9c498cf | ||
|
|
57b3b919a5 | ||
|
|
00c6bbb297 | ||
|
|
b6536a0af3 | ||
|
|
d08a2507fa | ||
|
|
8bc8829577 | ||
|
|
c843e6f68c | ||
|
|
84583e5501 | ||
|
|
4548562138 | ||
|
|
32c170b10a | ||
|
|
97dafa0a55 | ||
|
|
0be1ee46f2 | ||
|
|
34c9ab7643 | ||
|
|
59dbca250f | ||
|
|
4028dbfda1 | ||
|
|
b9dbf610b0 | ||
|
|
d443810520 | ||
|
|
fcd941d33d | ||
|
|
9c063fa37c | ||
|
|
2720cfe346 | ||
|
|
c39e38081e | ||
|
|
3deb4c3f42 | ||
|
|
6945091238 | ||
|
|
c758c4785a | ||
|
|
19269a20fb | ||
|
|
45669cacb1 | ||
|
|
840bc52aae | ||
|
|
bbc36e349f | ||
|
|
a4325adcdd | ||
|
|
23f39649d0 | ||
|
|
87b09a534e | ||
|
|
39f0e5ae0c | ||
|
|
62aaab0926 | ||
|
|
cddfe999aa | ||
|
|
fcbb658ac2 | ||
|
|
3bbf06ba49 | ||
|
|
d9be6f1d2e | ||
|
|
5d70e68a0b | ||
|
|
529f2325b2 | ||
|
|
314d433f86 | ||
|
|
12ea950c5f | ||
|
|
f4d12220ca | ||
|
|
6a9cba90f4 | ||
|
|
6873e1f1cb | ||
|
|
fa0a91a75d | ||
|
|
020bb659c5 | ||
|
|
b1d6687fb0 | ||
|
|
f67e17b287 | ||
|
|
81bd57c5ea | ||
|
|
d803bae874 | ||
|
|
14606f4087 | ||
|
|
599fdc7ee5 | ||
|
|
722e205db5 | ||
|
|
f67849eb47 | ||
|
|
662ca4e40a | ||
|
|
aa61be74d8 | ||
|
|
10296fcd6b | ||
|
|
f8bf146b6c | ||
|
|
52f104c517 | ||
|
|
6c1fc224f0 | ||
|
|
6b9ae3a8b3 | ||
|
|
07f73030c6 | ||
|
|
47130c79ee | ||
|
|
f3a3bdfe4f | ||
|
|
e5e54fe4c1 | ||
|
|
29c0f9a43a | ||
|
|
0b78229c77 | ||
|
|
c2a1d70070 | ||
|
|
260ecd1d9f | ||
|
|
3dce2e761a | ||
|
|
80a54200ce | ||
|
|
51227d438a | ||
|
|
6fb4199d37 | ||
|
|
6ba46aff6b | ||
|
|
5da34d0646 | ||
|
|
f215088939 | ||
|
|
df34dcdb0c | ||
|
|
89f464af99 | ||
|
|
3f6f02f7d2 | ||
|
|
0d861e5389 | ||
|
|
b290c8700c | ||
|
|
81b6fbe263 | ||
|
|
b3af293f66 | ||
|
|
b187485172 | ||
|
|
b449d9759c | ||
|
|
d9d63a3a2e | ||
|
|
fd7b54fb77 | ||
|
|
887f8a606d | ||
|
|
7e3717243f | ||
|
|
221849aa3a | ||
|
|
b52d40ab28 | ||
|
|
3ed68ffd92 | ||
|
|
cc3cd2c141 | ||
|
|
5e30f7efc4 | ||
|
|
35090251ef | ||
|
|
338afb4893 | ||
|
|
194d8a05f8 | ||
|
|
93e276bd9b | ||
|
|
a69517519c | ||
|
|
f646b1efb4 | ||
|
|
fc9bedacc0 | ||
|
|
795eeee809 | ||
|
|
6d7818962e | ||
|
|
068517c933 | ||
|
|
5b030200df | ||
|
|
c732122966 | ||
|
|
d7eb9b2d18 | ||
|
|
b8b09adda1 | ||
|
|
07c8f0c4b7 | ||
|
|
2bd201de63 | ||
|
|
0b7e118a37 | ||
|
|
a546769225 | ||
|
|
81745f932d | ||
|
|
4415bf31d2 | ||
|
|
5c1bcb41d8 | ||
|
|
b659c4c2bb | ||
|
|
65adc8a405 | ||
|
|
4141f78717 | ||
|
|
80cb02d206 | ||
|
|
a5a4510a1e | ||
|
|
95c30649d3 | ||
|
|
8e5cbde08c | ||
|
|
6df8632e29 | ||
|
|
3c1218fff1 | ||
|
|
69c0414791 | ||
|
|
d63f83fcbb | ||
|
|
75c3bf0c2f | ||
|
|
c9a8ab2389 | ||
|
|
2c467c00e1 | ||
|
|
c63ec5a1f2 | ||
|
|
e886558cbb | ||
|
|
8dd6dabe50 | ||
|
|
c090c6adf9 | ||
|
|
84da0befcd | ||
|
|
267751c8b9 | ||
|
|
8add9f7188 | ||
|
|
a100b0991b | ||
|
|
9ce9c5e535 | ||
|
|
b2d004ca1a | ||
|
|
657d50f9a3 | ||
|
|
60e355c4f5 | ||
|
|
adb444a60f | ||
|
|
e7e13ff70d | ||
|
|
a1e81db597 | ||
|
|
f23f2ff0a0 | ||
|
|
c1b18098f1 | ||
|
|
31c39592e3 | ||
|
|
82a1dad22a | ||
|
|
1ecec24727 | ||
|
|
607841e947 | ||
|
|
e234b403ae | ||
|
|
80ce7a36f8 | ||
|
|
705a8666be | ||
|
|
9167905118 | ||
|
|
bdeb6734d8 | ||
|
|
9a7b042594 | ||
|
|
7aea256fd8 | ||
|
|
857b5e6932 | ||
|
|
1a2d675439 | ||
|
|
0c749643de | ||
|
|
09bb1548f9 | ||
|
|
5ffe531844 | ||
|
|
fab24a3200 | ||
|
|
899d5e9d1d | ||
|
|
ba510884f2 | ||
|
|
78e8df8e17 | ||
|
|
deba1609c3 | ||
|
|
88d2425ca3 | ||
|
|
7117f9e058 | ||
|
|
c21c407416 | ||
|
|
4b4ad42063 | ||
|
|
474d514c7d | ||
|
|
6239466da8 | ||
|
|
7746d75582 | ||
|
|
642c9ded08 | ||
|
|
e0ae931ddd | ||
|
|
0d7727a405 | ||
|
|
28f689498a | ||
|
|
eb8fec7f2d | ||
|
|
2e16fa1d70 | ||
|
|
1b856c4909 | ||
|
|
585ad30af1 | ||
|
|
c0cdc4083c | ||
|
|
9b9db4f161 | ||
|
|
84a1d8d25e | ||
|
|
d3115a3bf3 | ||
|
|
964789e9a6 | ||
|
|
eeded51ff8 | ||
|
|
8f24f1b4d6 | ||
|
|
ad910a295a | ||
|
|
cf14c6b1e9 | ||
|
|
49da114caa | ||
|
|
b8376ebbf7 | ||
|
|
29701d7295 | ||
|
|
16279695a9 | ||
|
|
999fc86bc6 | ||
|
|
0276d533fb | ||
|
|
b77fc34a7b | ||
|
|
60c450d57e | ||
|
|
73411c75db | ||
|
|
8d146f7dff | ||
|
|
5c34aa0bb5 | ||
|
|
2b2ed8162d | ||
|
|
9770bd8005 | ||
|
|
4e020818ae | ||
|
|
58471c6971 | ||
|
|
2a2e02bf56 | ||
|
|
75d8cee766 | ||
|
|
1aed36bd16 | ||
|
|
00ce58ed18 | ||
|
|
d11aa1a61c | ||
|
|
56a62d3b4d | ||
|
|
e6dd668657 | ||
|
|
f60a64c8db | ||
|
|
eff1c298c9 | ||
|
|
358b0a122b | ||
|
|
c0f7ba9d46 | ||
|
|
c4edae8196 | ||
|
|
398dab808c | ||
|
|
3530871560 | ||
|
|
1ba26fdb98 | ||
|
|
a3b85b4e3e | ||
|
|
e37a5d0394 | ||
|
|
e5a8e77e2a | ||
|
|
314b59798f | ||
|
|
e9ae16e534 | ||
|
|
c971ca0ce2 | ||
|
|
0ad9a5f9c6 | ||
|
|
c31d91668a | ||
|
|
f5c196d717 | ||
|
|
3b90eed89f | ||
|
|
9828c8b787 | ||
|
|
b3e997134f | ||
|
|
f560baa69b | ||
|
|
8cf5f00c87 | ||
|
|
482c3895d3 | ||
|
|
fc0d4bde35 | ||
|
|
33ed89a036 | ||
|
|
0a5953c104 | ||
|
|
77f6be1a8b | ||
|
|
5bd3f9a571 | ||
|
|
ef59119663 | ||
|
|
45baca7018 | ||
|
|
9b1edb7a97 | ||
|
|
31c071d086 | ||
|
|
ecf4c5c104 | ||
|
|
35fbfece0d | ||
|
|
b7721e42d3 | ||
|
|
386346cee9 | ||
|
|
bbecccc45e | ||
|
|
1a8f84c134 | ||
|
|
66181fdcdf | ||
|
|
b9c05e8a9c | ||
|
|
9c22d6c12a | ||
|
|
f3cedbbd6f | ||
|
|
3f3a660ca1 | ||
|
|
1c6ded8416 | ||
|
|
aa63fdb26f | ||
|
|
3932330ce6 | ||
|
|
3b946b1c69 | ||
|
|
14df829f18 | ||
|
|
788d024be6 | ||
|
|
c20b56e089 | ||
|
|
287f4f239e | ||
|
|
dce66945ec | ||
|
|
92bd1d5200 | ||
|
|
06d2df8211 | ||
|
|
36256856b5 | ||
|
|
a771ae853c | ||
|
|
ef4e10bbb1 | ||
|
|
0dbe4d936e | ||
|
|
731fee11d4 | ||
|
|
6759df52c3 | ||
|
|
914b997076 | ||
|
|
0b8a2fea72 | ||
|
|
fb2538135c | ||
|
|
b4c547c278 | ||
|
|
b243bc846b | ||
|
|
6b8f6162b6 | ||
|
|
158db1532b | ||
|
|
6abfdb59c6 | ||
|
|
009d1f9ced | ||
|
|
555fba6598 | ||
|
|
f9017b72a7 | ||
|
|
99c3c2fc80 | ||
|
|
32381679f2 | ||
|
|
3d031265d1 | ||
|
|
026cda0071 | ||
|
|
fb41ed5a86 | ||
|
|
8a08468a73 | ||
|
|
f600cb4f2c | ||
|
|
f754f028dc | ||
|
|
41b292b45b | ||
|
|
af9be9cae8 | ||
|
|
ccfaea64c5 | ||
|
|
a86fc96730 | ||
|
|
cf51af17fd | ||
|
|
8c1b6a5cf0 | ||
|
|
bcecb8cd76 | ||
|
|
557790b0e5 | ||
|
|
8eb5a45718 | ||
|
|
7b64cef73b | ||
|
|
106203170e | ||
|
|
174d2bfc11 | ||
|
|
abda9c7f97 | ||
|
|
8e95260df9 | ||
|
|
5af1ae1920 | ||
|
|
f0eb9d48c9 | ||
|
|
0ac284009e | ||
|
|
fcf963639e | ||
|
|
ba8c0fb1d5 | ||
|
|
08fe74675b | ||
|
|
f5e7fdf8aa | ||
|
|
f6e447d049 | ||
|
|
531b21c012 | ||
|
|
a057456d5a | ||
|
|
0f043b39f5 | ||
|
|
dc0701e21d | ||
|
|
712f18f4e8 | ||
|
|
e0a82b4aaf | ||
|
|
a7f238ae0b | ||
|
|
0d99b6de7a | ||
|
|
6f627fca96 | ||
|
|
339fbc482b | ||
|
|
72e3ee1d77 | ||
|
|
a9750fb088 | ||
|
|
d80e3b0824 | ||
|
|
46df7a9ea0 | ||
|
|
b851ce49f7 | ||
|
|
d9afde3e15 | ||
|
|
b38c57f308 | ||
|
|
93e7e2e06e | ||
|
|
5ac09180a5 | ||
|
|
3819ca3a62 | ||
|
|
00426b4c9b | ||
|
|
58d8cefcc0 | ||
|
|
c8bb122557 | ||
|
|
bcef603a36 | ||
|
|
639b4d392a | ||
|
|
6d5f06a61d | ||
|
|
3e00e2ad58 | ||
|
|
cad2be5e53 | ||
|
|
58fe5f263f | ||
|
|
79ec6845f8 | ||
|
|
0f81ba8307 | ||
|
|
a30543b035 | ||
|
|
5c4473a1d9 | ||
|
|
7ff47c8c51 | ||
|
|
2544e29be3 | ||
|
|
d7bf564e8f | ||
|
|
0f135f881a | ||
|
|
2f1bb5e1c0 | ||
|
|
02b5f96eee | ||
|
|
3e77871539 | ||
|
|
676affdd03 | ||
|
|
5caf41c067 | ||
|
|
58f9e89fab | ||
|
|
8776f0f4a5 | ||
|
|
84a8c27926 | ||
|
|
f061e3486e | ||
|
|
da23995343 | ||
|
|
4e0a61bd9b | ||
|
|
d3e2fa5df5 | ||
|
|
00b111c974 | ||
|
|
bd265c00a0 | ||
|
|
ef53a63766 | ||
|
|
688c7f1a1c | ||
|
|
2989922253 | ||
|
|
6f6619a5ab | ||
|
|
1594c228e8 | ||
|
|
fb44f52aa9 | ||
|
|
045ead1728 | ||
|
|
4404463e53 | ||
|
|
b79b61c8c8 | ||
|
|
467048a0fc | ||
|
|
2a1edffce3 | ||
|
|
ce833c39d5 | ||
|
|
721a74eee6 | ||
|
|
8f9f4f894c | ||
|
|
842765dad0 | ||
|
|
b23cc47d95 | ||
|
|
85baa596d0 | ||
|
|
e371fff110 | ||
|
|
dffe6b4f39 | ||
|
|
dbf684f385 | ||
|
|
7b20fd91ef | ||
|
|
300c25ded1 | ||
|
|
284e814d2a | ||
|
|
d5cdaddeea | ||
|
|
8635b395a1 | ||
|
|
c2cf4e72f8 | ||
|
|
92def0f71d | ||
|
|
bccae9d71c | ||
|
|
90b6a2f82b | ||
|
|
0462df7de2 | ||
|
|
d4b29ab08d | ||
|
|
09f2dfe181 | ||
|
|
317d013a0b | ||
|
|
e26b2dcd43 | ||
|
|
4676dbc740 | ||
|
|
778f869ddb | ||
|
|
a7e9b1f76d | ||
|
|
096e56aaa8 | ||
|
|
d1bcc557f0 | ||
|
|
e041fab319 | ||
|
|
3394e36325 | ||
|
|
a501458e5a | ||
|
|
da08eef5ef | ||
|
|
0ea714552a | ||
|
|
8cba584e52 | ||
|
|
878f07d2cf | ||
|
|
d297de732f | ||
|
|
c41d4d32b9 | ||
|
|
5d34134888 | ||
|
|
f968ec4cac | ||
|
|
fdb256a534 | ||
|
|
62a2b57613 | ||
|
|
90f5ebfa58 | ||
|
|
66aecee519 | ||
|
|
f0c661d6e2 | ||
|
|
a27f5b4c15 | ||
|
|
95b69f0003 | ||
|
|
6925f0bf7a | ||
|
|
6c1a9ed83b | ||
|
|
3ebef79313 | ||
|
|
57393806b0 | ||
|
|
721d8cfa49 | ||
|
|
9e5b68444f | ||
|
|
9f6c619401 | ||
|
|
1b47e40a3a | ||
|
|
3fc14102e5 | ||
|
|
d907992c39 | ||
|
|
ae2f35c6c5 | ||
|
|
ddd804041d | ||
|
|
2478cbdb6f | ||
|
|
80e992a9fc | ||
|
|
e4b8e08e89 | ||
|
|
6c556b8a72 | ||
|
|
9659c19b23 | ||
|
|
aa6e9d9bf2 | ||
|
|
812f0ac32c | ||
|
|
296b312950 | ||
|
|
29a06406ea | ||
|
|
261cb7d3cd | ||
|
|
ba5a57ac07 | ||
|
|
c0ebe9d7a1 | ||
|
|
2b83012786 | ||
|
|
b9761288bd | ||
|
|
a711e83398 | ||
|
|
2705385681 | ||
|
|
4b2f3dd070 | ||
|
|
876acf2839 | ||
|
|
d0446f068c | ||
|
|
a9396d1e2f | ||
|
|
e87102e586 | ||
|
|
8b213f8d7c | ||
|
|
9a7dc5ba86 | ||
|
|
9acb3f83f8 | ||
|
|
1c676211ee | ||
|
|
89728164eb | ||
|
|
d0c4093f5a | ||
|
|
d7f680fb19 | ||
|
|
48279e060c | ||
|
|
10dd0d07dc | ||
|
|
d82dc4cf77 | ||
|
|
2b9553e4da | ||
|
|
ac112ea287 | ||
|
|
4f6b099615 | ||
|
|
d4dcb162d0 | ||
|
|
46054f513b | ||
|
|
261f67df50 | ||
|
|
22f9b2affe | ||
|
|
06c392c066 | ||
|
|
3d67b3bc17 | ||
|
|
6cefab5d8a | ||
|
|
cc261de37b | ||
|
|
20712641a7 | ||
|
|
e4d0b16fd5 | ||
|
|
f8c25791e9 | ||
|
|
704aa433d4 | ||
|
|
3bd1003164 | ||
|
|
8dd55d7506 | ||
|
|
6252d778c1 | ||
|
|
942248b9e6 | ||
|
|
4793449105 | ||
|
|
428e3bc0fc | ||
|
|
bf2c80cfcf | ||
|
|
06b0685a57 | ||
|
|
231ea25968 | ||
|
|
8658eeddb2 | ||
|
|
d0769eed97 | ||
|
|
b1fcd1f7c8 | ||
|
|
db1259b3e0 | ||
|
|
1a5f42b753 | ||
|
|
75d061a7fa | ||
|
|
9fb4c4140b | ||
|
|
0306877fb9 | ||
|
|
86e3b05a3f | ||
|
|
a4e8907c95 | ||
|
|
916ad6535a | ||
|
|
c129309937 | ||
|
|
0088e9ae77 | ||
|
|
79806b5ad5 | ||
|
|
7d59fbfc36 | ||
|
|
e645bdf249 | ||
|
|
0bedf26849 | ||
|
|
a153c5b4ce | ||
|
|
44f1f3e9ae | ||
|
|
8c82fa86c6 | ||
|
|
d4da934d6a | ||
|
|
56cc664c26 | ||
|
|
eaa0bdfc62 | ||
|
|
c538e9c6d4 | ||
|
|
54b9af0299 | ||
|
|
c7d5b9211c | ||
|
|
7ca22a8718 | ||
|
|
4e37b32976 | ||
|
|
9725b23db1 | ||
|
|
2e60f2b2ce | ||
|
|
004776a522 | ||
|
|
92fa1dde79 | ||
|
|
464821c4e2 | ||
|
|
e95483236a | ||
|
|
a9b97a85ad | ||
|
|
6170befc90 | ||
|
|
5ecb85cb6d | ||
|
|
d2fc04f45d | ||
|
|
fb4da933d4 | ||
|
|
7483900db2 | ||
|
|
9f78dbf200 | ||
|
|
ef9b9bdd6d | ||
|
|
1937aa43ba | ||
|
|
293ea66784 | ||
|
|
e98d8f4ced | ||
|
|
418d2afb2a | ||
|
|
a4c1a6187f | ||
|
|
123ca34040 | ||
|
|
6b3224116c | ||
|
|
635e0c9788 | ||
|
|
dd33a0e0ec | ||
|
|
191deeaba6 | ||
|
|
245072f7a2 | ||
|
|
6b858512b6 | ||
|
|
b857a01c30 | ||
|
|
94c9a3e05b | ||
|
|
8928d2c488 | ||
|
|
25bd5654aa | ||
|
|
83d5b96adf | ||
|
|
7eb90c5718 | ||
|
|
4b1af75724 | ||
|
|
8d07ab6527 | ||
|
|
ce4ea7e7a9 | ||
|
|
50ab5e7517 | ||
|
|
431c1d7f66 | ||
|
|
a55090dc2f | ||
|
|
d76cdb73b0 | ||
|
|
2594664330 | ||
|
|
f9ed075db6 | ||
|
|
099ced4f94 | ||
|
|
13d2513930 | ||
|
|
2211b1c65e | ||
|
|
1fd37ca2b2 | ||
|
|
7070e3748d | ||
|
|
dfaef908c2 | ||
|
|
67540c763b | ||
|
|
14269bd4d9 | ||
|
|
131663032c | ||
|
|
8ac71165e9 | ||
|
|
346758d3f0 | ||
|
|
d3e7f130fb | ||
|
|
aef8837b5d | ||
|
|
dc0832adba | ||
|
|
c0cd269322 | ||
|
|
0ad3ff655e | ||
|
|
ef45a62cc9 | ||
|
|
b79abbdea9 | ||
|
|
a9e4ce005d | ||
|
|
987f2b2a55 | ||
|
|
930e2d1d9d | ||
|
|
f4ada70e56 | ||
|
|
97e658709d | ||
|
|
ec2992cd2d | ||
|
|
619208565b | ||
|
|
dcd689d2ea | ||
|
|
e94de15f83 | ||
|
|
6af7de51a5 | ||
|
|
559c6722ff | ||
|
|
aab2cce978 | ||
|
|
f4a4af0fa4 | ||
|
|
6934838974 | ||
|
|
1aadd25cb5 | ||
|
|
0caf944668 | ||
|
|
6452f62b88 | ||
|
|
e061dfd808 | ||
|
|
4da53ef219 | ||
|
|
347e44f04d | ||
|
|
8997fa7242 | ||
|
|
19ba6efb82 | ||
|
|
d10cbc9984 | ||
|
|
6c7d9ded00 | ||
|
|
6d04e89d7d | ||
|
|
2beb24147d | ||
|
|
16c5f4e377 | ||
|
|
03a6f1753c | ||
|
|
9fb61d8446 | ||
|
|
bc3322d3c9 | ||
|
|
06c7bf7514 | ||
|
|
4c89a000e4 | ||
|
|
86d61e0b44 | ||
|
|
6407390d72 | ||
|
|
648120cabf | ||
|
|
ce5b3f290a | ||
|
|
5308ca1806 | ||
|
|
6df6d408d2 | ||
|
|
b60d6ccdd8 | ||
|
|
de01c9685e | ||
|
|
31d2ecc9fd | ||
|
|
2f8502aec6 | ||
|
|
d377b04dad | ||
|
|
40cc78ae1e | ||
|
|
268f1e8472 | ||
|
|
004b7c782d | ||
|
|
33b293f0aa | ||
|
|
ad584a98ad | ||
|
|
e2509eddb2 | ||
|
|
b9f72d0e78 | ||
|
|
c839bb2db3 | ||
|
|
30161369a8 | ||
|
|
c65aa9732e | ||
|
|
bc72b8fd1c | ||
|
|
f089531bd1 | ||
|
|
8d8ea53804 | ||
|
|
89e405e927 | ||
|
|
ca984a6630 | ||
|
|
fa39a55eca | ||
|
|
c3a1ba2f2d | ||
|
|
86e291f250 | ||
|
|
dd1d4439a9 | ||
|
|
cbfde18f8c | ||
|
|
e2c2e23d2a | ||
|
|
40cc5d5242 | ||
|
|
9765194ace | ||
|
|
628465e6b5 | ||
|
|
58706df120 | ||
|
|
b19225c747 | ||
|
|
c304889e61 | ||
|
|
05a9204678 | ||
|
|
ed8537bb0b | ||
|
|
6a9ae10fcf | ||
|
|
05358904bf | ||
|
|
1a6901c3e3 | ||
|
|
7aaba8244b | ||
|
|
8c45dcde88 | ||
|
|
6c155b04b2 | ||
|
|
cd8ad9a2ec | ||
|
|
a5db7d0246 | ||
|
|
b84b467b96 | ||
|
|
0812aaac88 | ||
|
|
194d2f911e | ||
|
|
e360b36b8a | ||
|
|
b6f66dd287 | ||
|
|
0a4bb48cd3 | ||
|
|
15d62d4a91 | ||
|
|
5b13c44ef9 | ||
|
|
0a4250f3b4 | ||
|
|
f79223ed58 | ||
|
|
2d28218a2a | ||
|
|
35974f2ee1 | ||
|
|
1f73323fb9 | ||
|
|
a3d0736eec | ||
|
|
4bdd486c00 | ||
|
|
c3895c9bd7 | ||
|
|
e9ddd89b32 | ||
|
|
88a8f2d609 | ||
|
|
a5dc5c89e8 | ||
|
|
3a15a35137 | ||
|
|
b644640804 | ||
|
|
aaa4f66671 | ||
|
|
07e021199e | ||
|
|
6b2ca7dc80 | ||
|
|
091d62803e | ||
|
|
547999bae0 | ||
|
|
99013f7998 | ||
|
|
fc396800db | ||
|
|
6d03ae57ac | ||
|
|
4a0aa57355 | ||
|
|
7db737494c | ||
|
|
b285501c44 | ||
|
|
2f9b29994f | ||
|
|
917434cb6b | ||
|
|
28a52bb658 | ||
|
|
82bc19374c | ||
|
|
0b23f30bb7 | ||
|
|
64a62d7aed | ||
|
|
de31cf8e7d | ||
|
|
3484f9afb3 | ||
|
|
81df0ff390 | ||
|
|
d403ec7399 | ||
|
|
6ac77835df | ||
|
|
b113119a9a | ||
|
|
b713057614 | ||
|
|
4268570166 | ||
|
|
ead508c0d0 | ||
|
|
f8e1be8565 | ||
|
|
360f1af32f | ||
|
|
49a08d14c3 | ||
|
|
d897df6a30 | ||
|
|
ba4f3a1553 | ||
|
|
6ba9534da4 | ||
|
|
c16ef96754 | ||
|
|
e728491aa2 | ||
|
|
ce356fa266 | ||
|
|
5e46323ca3 | ||
|
|
0a7d047246 | ||
|
|
3fa534a3eb | ||
|
|
25990f59d8 | ||
|
|
c6405f70d3 | ||
|
|
acae6c2c49 | ||
|
|
141fdc2197 | ||
|
|
a7ed8a006f | ||
|
|
b1a0ebd531 | ||
|
|
e8021acccd | ||
|
|
39b0da2a3f | ||
|
|
fd3d18f6c5 | ||
|
|
ecc27d1674 | ||
|
|
7d0514ab36 | ||
|
|
44c3024c00 | ||
|
|
253c92bab7 | ||
|
|
c10850118d | ||
|
|
4f017e9173 | ||
|
|
5ed46c82cb | ||
|
|
64391e906d | ||
|
|
47b4ee07ab | ||
|
|
3000cbf763 | ||
|
|
76b3d314a8 | ||
|
|
ba646de0ad | ||
|
|
395f746a05 | ||
|
|
f7e57cd398 | ||
|
|
3ea6d97ed2 | ||
|
|
affc0d8b67 | ||
|
|
c637e310e9 | ||
|
|
6ee7dcdd51 | ||
|
|
23470267fe | ||
|
|
4a92bb91df | ||
|
|
69522c422c | ||
|
|
bc5e3524eb | ||
|
|
479297fc35 | ||
|
|
516feafcfb | ||
|
|
a135c82ab5 | ||
|
|
10996f1cbd | ||
|
|
23b060e1f5 | ||
|
|
622ff3a256 | ||
|
|
5d457b6834 | ||
|
|
f10f76d127 | ||
|
|
58f3382daf | ||
|
|
0e1139446e | ||
|
|
f433216fae | ||
|
|
ed680baaac | ||
|
|
e0a9d908ed | ||
|
|
bfa4a46bd5 | ||
|
|
03f3ff991e | ||
|
|
619b4824f0 | ||
|
|
021af0186b | ||
|
|
d3caad8b8d | ||
|
|
ec6bec3326 | ||
|
|
dd54740d36 | ||
|
|
8f65156bda | ||
|
|
96c7df5afa | ||
|
|
0c19105fbf | ||
|
|
4145d83248 | ||
|
|
6490705e2a | ||
|
|
10d2432df5 | ||
|
|
815db72671 | ||
|
|
6d0ba61c54 | ||
|
|
5f61267f75 | ||
|
|
94ee42cebb | ||
|
|
b6795e5c63 | ||
|
|
ef85d063c2 | ||
|
|
59755971e5 | ||
|
|
c5ab831a87 | ||
|
|
6715dc2a5d | ||
|
|
af6de64ec0 | ||
|
|
1ac2448f90 | ||
|
|
b5f34b30d3 | ||
|
|
01f4e080df | ||
|
|
d55335e70b | ||
|
|
a8c1dc4bc6 | ||
|
|
2897059503 | ||
|
|
d491f9df5a | ||
|
|
bc40318e40 | ||
|
|
3935434f04 | ||
|
|
4cf1f2de94 | ||
|
|
73156c6780 | ||
|
|
bc52bafa8d | ||
|
|
5c9007b242 | ||
|
|
5857e3f75e | ||
|
|
e202831013 | ||
|
|
4cbbfccb6d | ||
|
|
21f3b1cf34 | ||
|
|
f7b384e9b6 | ||
|
|
1e6ab47ee4 | ||
|
|
78341ea2f1 | ||
|
|
3f8a4d4273 | ||
|
|
bae517c9f8 | ||
|
|
c88ccbf9bc | ||
|
|
5e40f5d509 | ||
|
|
46389131bc | ||
|
|
c6a344d0d9 | ||
|
|
bcc2c377a0 | ||
|
|
bb6afc847e | ||
|
|
e0193151db | ||
|
|
42a80bad8e | ||
|
|
6e3e77f65d | ||
|
|
e155f022a0 | ||
|
|
db65aab347 | ||
|
|
a180c5f357 | ||
|
|
1c0279f17c | ||
|
|
8866eb292b | ||
|
|
6fdda3391e | ||
|
|
fdb8dd4e5b | ||
|
|
9a1d3783ee | ||
|
|
3841d9e322 | ||
|
|
e392eadf8a | ||
|
|
f743d5d0b5 | ||
|
|
4a76bf59ef | ||
|
|
205b29e2f5 | ||
|
|
d511b82264 | ||
|
|
aaae112e60 | ||
|
|
955fd6207f | ||
|
|
4e56c96612 | ||
|
|
dd046f3442 | ||
|
|
5a947f83a1 | ||
|
|
b87b8b54fd | ||
|
|
233c0537a1 | ||
|
|
63d4798a50 | ||
|
|
6c47517684 | ||
|
|
c58b1a0143 | ||
|
|
f489d9131b | ||
|
|
f0109c5588 | ||
|
|
c16becba56 | ||
|
|
4605788696 | ||
|
|
87908313cc | ||
|
|
9cc2eba7b8 | ||
|
|
2459cee57b | ||
|
|
0bf6ce57ed | ||
|
|
7041424f96 | ||
|
|
9509285c16 | ||
|
|
e55ee0e65d | ||
|
|
9ea70497c2 | ||
|
|
3389b9e9fd | ||
|
|
76d4d54639 | ||
|
|
1b692b6c37 | ||
|
|
40d8cef1a2 | ||
|
|
23550c0062 | ||
|
|
949bd940ee | ||
|
|
79bdb9eed5 | ||
|
|
a141f08298 | ||
|
|
dee43a3911 | ||
|
|
ef227d0139 | ||
|
|
cbcf9ce645 | ||
|
|
0e5af2b16c | ||
|
|
85ca3a3b27 | ||
|
|
fc5f5f3b6c | ||
|
|
716fd8c0b9 | ||
|
|
a517393c43 | ||
|
|
c2311faffe | ||
|
|
fe453b0d66 | ||
|
|
7e75b0fc02 | ||
|
|
11b0a0a73d | ||
|
|
82fdb5c3eb | ||
|
|
3f1d532c8b | ||
|
|
f258b00aa7 | ||
|
|
4e71b9576d | ||
|
|
f36567a5cd | ||
|
|
924ebb6c7f | ||
|
|
6e7e8eb44a | ||
|
|
308c583254 | ||
|
|
97b2f7e5ca | ||
|
|
3ea88a07d9 | ||
|
|
588f8bb96a | ||
|
|
c93c0dd721 | ||
|
|
fc59c254fd | ||
|
|
2f8b6a150f | ||
|
|
db60ac5c17 | ||
|
|
e1f09853c5 | ||
|
|
24656713a5 | ||
|
|
7dd0269292 | ||
|
|
8b87cea7aa | ||
|
|
c7559a6946 | ||
|
|
945c6080ad | ||
|
|
44590965d1 | ||
|
|
7ab64d678f | ||
|
|
e406a76b62 | ||
|
|
e26f175a8f | ||
|
|
d4ab84745d | ||
|
|
32dbc3101e | ||
|
|
0a924eb718 | ||
|
|
a284327bfc | ||
|
|
2ea38d6ecc | ||
|
|
6a34bbfddd | ||
|
|
58323ada4b | ||
|
|
5fd723cb80 | ||
|
|
5c626e6957 | ||
|
|
5d949842eb | ||
|
|
b595c17d78 | ||
|
|
b84973ba2b | ||
|
|
61be49e7b2 | ||
|
|
8faf5659ee | ||
|
|
cc9267a646 | ||
|
|
55838bb032 | ||
|
|
67619ac5e8 | ||
|
|
952b342859 | ||
|
|
c7149c460d | ||
|
|
fd0613ea0e | ||
|
|
36d2dddc59 | ||
|
|
63c5b05584 | ||
|
|
4b235e5b87 | ||
|
|
6c51fffdaa | ||
|
|
5d6d638c85 | ||
|
|
90eb515167 | ||
|
|
17526711a2 | ||
|
|
cf0118e090 | ||
|
|
868d6fec42 | ||
|
|
851f5854bf | ||
|
|
eb5428c971 | ||
|
|
81188df7ef | ||
|
|
9fd365cc41 | ||
|
|
999df6e40f | ||
|
|
076d069568 | ||
|
|
2738648197 | ||
|
|
36013009a1 | ||
|
|
1b60233862 | ||
|
|
2cba10dd05 | ||
|
|
b3944127ea | ||
|
|
f1674378ca | ||
|
|
6f0191e1cf | ||
|
|
1848844be6 | ||
|
|
8b6362c749 | ||
|
|
d860d13361 | ||
|
|
4b077dbf4c | ||
|
|
40f73bbfe2 | ||
|
|
f455706d7c | ||
|
|
23e9672476 | ||
|
|
36f992f95f | ||
|
|
b2c6d526ab | ||
|
|
fe1e833677 | ||
|
|
8df1b9e8e5 | ||
|
|
38b0f71b01 | ||
|
|
29d2f115f8 | ||
|
|
0f677b4891 | ||
|
|
2f7dd04168 | ||
|
|
ed3b667985 | ||
|
|
6ae1d8c158 | ||
|
|
404bced97b | ||
|
|
5af49c8a82 | ||
|
|
85aa98e8e2 | ||
|
|
330d102f62 | ||
|
|
32b33a7910 | ||
|
|
17c6a0f28a | ||
|
|
7341eed1cf | ||
|
|
ff99fbfbc9 | ||
|
|
9f67fdc771 | ||
|
|
521143a16b | ||
|
|
2622a25b12 | ||
|
|
a91e925221 | ||
|
|
6c3289d5a5 | ||
|
|
988a91ac06 | ||
|
|
aa7c913e9a | ||
|
|
56db9feaa4 | ||
|
|
5ace0f13c9 | ||
|
|
076e6c9479 | ||
|
|
8277b1192e | ||
|
|
150b978b0e | ||
|
|
6c72096bfe | ||
|
|
87c18cea80 | ||
|
|
e658734084 | ||
|
|
ec4f350baa | ||
|
|
095f583211 | ||
|
|
3c864cf6d2 | ||
|
|
eb4b21ce9f | ||
|
|
ff5349fd90 | ||
|
|
1f34ffa85d | ||
|
|
e98cab1f7c | ||
|
|
aabc9659a2 | ||
|
|
8d8d308f7a | ||
|
|
3ebd4595c6 | ||
|
|
7e1168946f | ||
|
|
134689d8aa | ||
|
|
56282f9cbb | ||
|
|
b4713741b1 | ||
|
|
e42fe3bd61 | ||
|
|
4fd2dade60 | ||
|
|
e12b03504c | ||
|
|
153156c1fa | ||
|
|
3ecc69da2b | ||
|
|
07ad29da41 | ||
|
|
7d0de0b26f | ||
|
|
77fab9c78f | ||
|
|
3a8f3272c7 | ||
|
|
2d44cbac1b | ||
|
|
893d72677b | ||
|
|
979eca4066 | ||
|
|
258d13e746 | ||
|
|
779531da5d | ||
|
|
31d71006d7 | ||
|
|
64ca66c062 | ||
|
|
6e1a2b3427 | ||
|
|
f585235192 | ||
|
|
9355643554 | ||
|
|
ccc6055926 | ||
|
|
6639446bb8 | ||
|
|
e2925c585f | ||
|
|
6c76b0473c | ||
|
|
e1e19632a5 | ||
|
|
3e5364d5c0 | ||
|
|
6c98de4c8b | ||
|
|
9613dde4d2 | ||
|
|
d47df2e538 | ||
|
|
6fcacd5159 | ||
|
|
11b39cb020 | ||
|
|
d81f132db6 | ||
|
|
095697e789 | ||
|
|
62d98c3137 | ||
|
|
e80d5dc172 | ||
|
|
421e29db2d | ||
|
|
9e6e53583c | ||
|
|
3f59a7d84e | ||
|
|
21ffd788ab | ||
|
|
8dadfea724 | ||
|
|
00ce52ecf7 | ||
|
|
50ac13d3fd | ||
|
|
58318fec46 | ||
|
|
a49941113e | ||
|
|
595801cb99 | ||
|
|
0b469f09df | ||
|
|
1e1f4e4a47 | ||
|
|
c63e2ae7c8 | ||
|
|
d3d3fa990e | ||
|
|
21980b7e71 | ||
|
|
844ca0d387 | ||
|
|
972ae35300 | ||
|
|
57bfb8eb96 | ||
|
|
ed6e6a9fb2 | ||
|
|
ed402267b6 | ||
|
|
6eec570828 | ||
|
|
22fc1e3f0b | ||
|
|
ae9bd868f1 | ||
|
|
a887012aca | ||
|
|
bc73048ab9 | ||
|
|
c89dd6c379 | ||
|
|
9662debe5e | ||
|
|
057262d917 | ||
|
|
b6723a6219 | ||
|
|
068f3e0a43 | ||
|
|
95635a8c47 | ||
|
|
3ec2071820 | ||
|
|
1696db3044 | ||
|
|
e1a1eab2b3 | ||
|
|
f7865f3358 | ||
|
|
6d5f8ed5f3 | ||
|
|
96a737379f | ||
|
|
d73feec013 | ||
|
|
2ccead1da5 | ||
|
|
8885f2717e | ||
|
|
4448ffc777 | ||
|
|
022d10c598 | ||
|
|
8e6b7043bd | ||
|
|
66eaaff598 | ||
|
|
478c6c134f | ||
|
|
b5d333ba6c | ||
|
|
81723d55ac | ||
|
|
fb784ce962 | ||
|
|
5a37380900 | ||
|
|
b6300f3a5c | ||
|
|
a3e8a2d623 | ||
|
|
7b3a4bdc39 | ||
|
|
cc0b5e5e0f | ||
|
|
5c3f7d8f94 | ||
|
|
8c3f8cd450 | ||
|
|
046582711a | ||
|
|
15756ec92d | ||
|
|
fc49abc9fb | ||
|
|
4a9ff27f3e | ||
|
|
790e6f370f | ||
|
|
16ccc1321d | ||
|
|
8648c94dd4 | ||
|
|
dc4eb720ae | ||
|
|
0b891ad557 | ||
|
|
e96193ae28 | ||
|
|
3ff9075959 | ||
|
|
c03842056c | ||
|
|
6df226b21c | ||
|
|
7dfa7d7426 | ||
|
|
b8b1a891cf | ||
|
|
7df0e8b0f9 | ||
|
|
ff072ae9d9 | ||
|
|
f81ca39741 | ||
|
|
3db1f2a98c | ||
|
|
4865df9be1 | ||
|
|
0c16f2c334 | ||
|
|
d01149620f | ||
|
|
ab9401f390 | ||
|
|
3223c17b74 | ||
|
|
404035bcf0 | ||
|
|
a0185bb0b4 | ||
|
|
1a591cd9f1 | ||
|
|
e9b81b2033 | ||
|
|
cbfc1e8ed1 | ||
|
|
cb63338805 | ||
|
|
bcdc82ccee | ||
|
|
76a4cf6c34 | ||
|
|
872f23b0f0 | ||
|
|
e61f7405fd | ||
|
|
0714871b56 | ||
|
|
8a89fb2a1a | ||
|
|
036544e3ed | ||
|
|
7a6784d809 | ||
|
|
ed9301705b | ||
|
|
21f9694574 | ||
|
|
3a0b11b89d |
@@ -1,9 +1,37 @@
|
||||
^\.Rproj\.user$
|
||||
^\.git$
|
||||
^examples$
|
||||
^README\.md$
|
||||
^shiny\.Rproj$
|
||||
^shiny\.sh$
|
||||
^shiny\.cmd$
|
||||
^run\.R$
|
||||
^\.gitignore$
|
||||
^smoketests$
|
||||
^res$
|
||||
^man-roxygen$
|
||||
^\.travis\.yml$
|
||||
^staticdocs$
|
||||
^tools$
|
||||
^srcts$
|
||||
^CONTRIBUTING.md$
|
||||
^cran-comments.md$
|
||||
^.*\.o$
|
||||
^appveyor\.yml$
|
||||
^revdep$
|
||||
^TODO-promises.md$
|
||||
^manualtests$
|
||||
^\.github$
|
||||
^\.vscode$
|
||||
^\.madgerc$
|
||||
^package\.json$
|
||||
^tsconfig\.json$
|
||||
^package-lock\.json$
|
||||
^node_modules$
|
||||
^coverage$
|
||||
^.ignore$
|
||||
^eslint\.config\.mjs$
|
||||
^_dev$
|
||||
^.claude$
|
||||
^README-npm\.md$
|
||||
^CRAN-SUBMISSION$
|
||||
^LICENSE\.md$
|
||||
|
||||
2
.Rinstignore
Normal file
2
.Rinstignore
Normal file
@@ -0,0 +1,2 @@
|
||||
^tools$
|
||||
^Rmd$
|
||||
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/NEWS merge=union
|
||||
/inst/www/shared/shiny.js -merge -diff
|
||||
/inst/www/shared/shiny-*.js -merge -diff
|
||||
/inst/www/shared/shiny*.css -merge -diff
|
||||
*.min.js -merge -diff
|
||||
*.js.map -merge -diff
|
||||
40
.github/CONTRIBUTING.md
vendored
Normal file
40
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
We welcome contributions to the **shiny** package. To submit a contribution:
|
||||
|
||||
1. [Fork](https://github.com/rstudio/shiny/fork) the repository and make your changes.
|
||||
|
||||
2. Submit a [pull request](https://help.github.com/articles/using-pull-requests).
|
||||
|
||||
3. Ensure that you have signed the contributor license agreement. It will appear as a "Check"
|
||||
on your PR and a comment from "CLAassistant" will also appear explaining whether you have
|
||||
yet to sign. After you sign, you can click the "Recheck" link in that comment and the check
|
||||
will flip to reflect that you've signed.
|
||||
|
||||
We generally do not merge pull requests that update included web libraries (such as Bootstrap or jQuery) because it is difficult for us to verify that the update is done correctly; we prefer to update these libraries ourselves.
|
||||
|
||||
## How to make changes
|
||||
|
||||
Before you submit a pull request, please do the following:
|
||||
|
||||
* Add an entry to NEWS.md concisely describing what you changed.
|
||||
|
||||
* If appropriate, add unit tests in the tests/ directory.
|
||||
|
||||
* If you made any changes to the JavaScript files in the srcjs/ directory, make sure you build the output JavaScript files. See tools/README.md file for information on using the build system.
|
||||
|
||||
* Run Build->Check Package in the RStudio IDE, or `devtools::check()`, to make sure your change did not add any messages, warnings, or errors.
|
||||
|
||||
Doing these things will make it easier for the Shiny development team to evaluate your pull request. Even so, we may still decide to modify your code or even not merge it at all. Factors that may prevent us from merging the pull request include:
|
||||
|
||||
* breaking backward compatibility
|
||||
* adding a feature that we do not consider relevant for Shiny
|
||||
* is hard to understand
|
||||
* is hard to maintain in the future
|
||||
* is computationally expensive
|
||||
* is not intuitive for people to use
|
||||
|
||||
We will try to be responsive and provide feedback in case we decide not to merge your pull request.
|
||||
|
||||
|
||||
## Filing issues
|
||||
|
||||
If you find a bug in Shiny, you can also [file an issue](https://github.com/rstudio/shiny/issues/new). Please provide as much relevant information as you can, and include a minimal reproducible example if possible.
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name : Bug report
|
||||
about : Report a bug in Shiny.
|
||||
---
|
||||
|
||||
<!--
|
||||
This issue tracker is for bugs and feature requests in the Shiny package. If you're having trouble with Shiny Server or a related package, please file an issue in the appropriate repository.
|
||||
|
||||
If you're having trouble with shinyapps.io, and you have a paid account (Starter, Basic, Standard, or Pro), please file a support ticket via https://support.rstudio.com. If you have a Free account, please post to the RStudio Community with the shinyappsio tag: https://community.rstudio.com/tags/shinyappsio.
|
||||
|
||||
Finally, if you are an RStudio customer and are having trouble with one of our Pro products, get in touch with our support team at support@rstudio.com.
|
||||
|
||||
Before you file an issue, please upgrade to the latest version of Shiny from CRAN and confirm that the problem persists.
|
||||
|
||||
# First, restart R.
|
||||
# To install latest shiny from CRAN:
|
||||
install.packages("shiny")
|
||||
|
||||
See our guide to writing good bug reports for further guidance: https://github.com/rstudio/shiny/wiki/Writing-Good-Bug-Reports. The better your report is, the likelier we are to be able to reproduce and ultimately solve it.
|
||||
-->
|
||||
|
||||
### System details
|
||||
|
||||
Browser Version: <!-- If applicable -->
|
||||
|
||||
Output of `sessionInfo()`:
|
||||
|
||||
```
|
||||
# sessionInfo() output goes here
|
||||
```
|
||||
|
||||
### Example application *or* steps to reproduce the problem
|
||||
|
||||
<!-- If you're able to create one, a reproducible example is extremely helpful to us. For instructions on how to create one, please see: https://github.com/rstudio/shiny/wiki/Creating-a-Reproducible-Example -->
|
||||
|
||||
```R
|
||||
# Minimal, self-contained example app code goes here
|
||||
```
|
||||
|
||||
### Describe the problem in detail
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name : Feature request
|
||||
about : Request a new feature.
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Thanks for taking the time to file a feature request! Please take the time to search for an existing feature request, to avoid creating duplicate requests. If you find an existing feature request, please give it a thumbs-up reaction, as we'll use these reactions to help prioritize the implementation of these features in the future.
|
||||
|
||||
If the feature has not yet been filed, then please describe the feature you'd like to see become a part of Shiny. See:
|
||||
|
||||
https://github.com/rstudio/shiny/wiki/Writing-Good-Feature-Requests
|
||||
|
||||
for a guide on how to write good feature requests.
|
||||
|
||||
-->
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name : Ask a Question
|
||||
about : The issue tracker is not for questions -- please ask questions at https://forum.posit.co/tags/shiny.
|
||||
---
|
||||
|
||||
The issue tracker is not for questions. If you have a question, please feel free to ask it on our community site, at https://forum.posit.co/c/shiny.
|
||||
|
||||
13
.github/shiny-workflows/routine.sh
vendored
Normal file
13
.github/shiny-workflows/routine.sh
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash -e
|
||||
. ./tools/documentation/checkDocsCurrent.sh
|
||||
|
||||
echo "Updating package.json version to match DESCRIPTION Version"
|
||||
Rscript ./tools/updatePackageJsonVersion.R
|
||||
if [ -n "$(git status --porcelain package.json)" ]
|
||||
then
|
||||
echo "package.json has changed after running ./tools/updatePackageJsonVersion.R. Re-running 'npm run build'"
|
||||
npm run build
|
||||
git add ./inst package.json && git commit -m 'Sync package version (GitHub Actions)' || echo "No package version to commit"
|
||||
else
|
||||
echo "No package version difference detected; package.json is current."
|
||||
fi
|
||||
25
.github/workflows/R-CMD-check.yaml
vendored
Normal file
25
.github/workflows/R-CMD-check.yaml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Workflow derived from https://github.com/rstudio/shiny-workflows
|
||||
#
|
||||
# NOTE: This Shiny team GHA workflow is overkill for most R packages.
|
||||
# For most R packages it is better to use https://github.com/r-lib/actions
|
||||
on:
|
||||
push:
|
||||
branches: [main, rc-**]
|
||||
pull_request:
|
||||
branches:
|
||||
schedule:
|
||||
- cron: "0 5 * * 1" # every monday
|
||||
|
||||
name: Package checks
|
||||
|
||||
jobs:
|
||||
website:
|
||||
uses: rstudio/shiny-workflows/.github/workflows/website.yaml@v1
|
||||
routine:
|
||||
uses: rstudio/shiny-workflows/.github/workflows/routine.yaml@v1
|
||||
R-CMD-check:
|
||||
uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1
|
||||
with:
|
||||
# On R 4.2, Cairo has difficulty installing
|
||||
# Remove this line when https://github.com/s-u/Cairo/issues/52 is merged
|
||||
extra-packages: Cairo=?ignore
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -6,4 +6,19 @@
|
||||
*.so
|
||||
/src-i386/
|
||||
/src-x86_64/
|
||||
shinyapps/
|
||||
README.html
|
||||
.*.Rnb.cached
|
||||
/_dev/
|
||||
.sass_cache_keys
|
||||
|
||||
# TypeScript
|
||||
/node_modules/
|
||||
.cache
|
||||
coverage/
|
||||
madge.svg
|
||||
|
||||
|
||||
# GHA remotes installation
|
||||
.github/r-depends.rds
|
||||
.claude/settings.local.json
|
||||
|
||||
7
.madgerc
Normal file
7
.madgerc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"detectiveOptions": {
|
||||
"ts": {
|
||||
"skipTypeImports": true
|
||||
}
|
||||
}
|
||||
}
|
||||
6
.vscode/extensions.json
vendored
Normal file
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
23
.vscode/settings.json
vendored
Normal file
23
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
},
|
||||
"prettier.prettierPath": "./node_modules/prettier",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"[r]": {
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.insertFinalNewline": true,
|
||||
"editor.formatOnSave": false,
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.insertFinalNewline": true,
|
||||
},
|
||||
"[json]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.insertFinalNewline": true,
|
||||
},
|
||||
}
|
||||
242
DESCRIPTION
242
DESCRIPTION
@@ -1,27 +1,229 @@
|
||||
Package: shiny
|
||||
Type: Package
|
||||
Package: shiny
|
||||
Title: Web Application Framework for R
|
||||
Version: 0.1.4
|
||||
Date: 2012-08-30
|
||||
Author: RStudio, Inc.
|
||||
Maintainer: Joe Cheng <joe@rstudio.org>
|
||||
Description: Shiny makes it incredibly easy to build interactive web
|
||||
applications with R. Automatic "reactive" binding between inputs and
|
||||
outputs and extensive pre-built widgets make it possible to build
|
||||
Version: 1.12.1.9000
|
||||
Authors@R: c(
|
||||
person("Winston", "Chang", , "winston@posit.co", role = "aut",
|
||||
comment = c(ORCID = "0000-0002-1576-2126")),
|
||||
person("Joe", "Cheng", , "joe@posit.co", role = "aut"),
|
||||
person("JJ", "Allaire", , "jj@posit.co", role = "aut"),
|
||||
person("Carson", "Sievert", , "carson@posit.co", role = c("aut", "cre"),
|
||||
comment = c(ORCID = "0000-0002-4958-2844")),
|
||||
person("Barret", "Schloerke", , "barret@posit.co", role = "aut",
|
||||
comment = c(ORCID = "0000-0001-9986-114X")),
|
||||
person("Garrick", "Aden-Buie", , "garrick@adenbuie.com", role = "aut",
|
||||
comment = c(ORCID = "0000-0002-7111-0077")),
|
||||
person("Yihui", "Xie", , "yihui@posit.co", role = "aut"),
|
||||
person("Jeff", "Allen", role = "aut"),
|
||||
person("Jonathan", "McPherson", , "jonathan@posit.co", role = "aut"),
|
||||
person("Alan", "Dipert", role = "aut"),
|
||||
person("Barbara", "Borges", role = "aut"),
|
||||
person("Posit Software, PBC", role = c("cph", "fnd"),
|
||||
comment = c(ROR = "03wc8by49")),
|
||||
person(, "jQuery Foundation", role = "cph",
|
||||
comment = "jQuery library and jQuery UI library"),
|
||||
person(, "jQuery contributors", role = c("ctb", "cph"),
|
||||
comment = "jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt"),
|
||||
person(, "jQuery UI contributors", role = c("ctb", "cph"),
|
||||
comment = "jQuery UI library; authors listed in inst/www/shared/jqueryui/AUTHORS.txt"),
|
||||
person("Mark", "Otto", role = "ctb",
|
||||
comment = "Bootstrap library"),
|
||||
person("Jacob", "Thornton", role = "ctb",
|
||||
comment = "Bootstrap library"),
|
||||
person(, "Bootstrap contributors", role = "ctb",
|
||||
comment = "Bootstrap library"),
|
||||
person(, "Twitter, Inc", role = "cph",
|
||||
comment = "Bootstrap library"),
|
||||
person("Prem Nawaz", "Khan", role = "ctb",
|
||||
comment = "Bootstrap accessibility plugin"),
|
||||
person("Victor", "Tsaran", role = "ctb",
|
||||
comment = "Bootstrap accessibility plugin"),
|
||||
person("Dennis", "Lembree", role = "ctb",
|
||||
comment = "Bootstrap accessibility plugin"),
|
||||
person("Srinivasu", "Chakravarthula", role = "ctb",
|
||||
comment = "Bootstrap accessibility plugin"),
|
||||
person("Cathy", "O'Connor", role = "ctb",
|
||||
comment = "Bootstrap accessibility plugin"),
|
||||
person(, "PayPal, Inc", role = "cph",
|
||||
comment = "Bootstrap accessibility plugin"),
|
||||
person("Stefan", "Petre", role = c("ctb", "cph"),
|
||||
comment = "Bootstrap-datepicker library"),
|
||||
person("Andrew", "Rowls", role = c("ctb", "cph"),
|
||||
comment = "Bootstrap-datepicker library"),
|
||||
person("Brian", "Reavis", role = c("ctb", "cph"),
|
||||
comment = "selectize.js library"),
|
||||
person("Salmen", "Bejaoui", role = c("ctb", "cph"),
|
||||
comment = "selectize-plugin-a11y library"),
|
||||
person("Denis", "Ineshin", role = c("ctb", "cph"),
|
||||
comment = "ion.rangeSlider library"),
|
||||
person("Sami", "Samhuri", role = c("ctb", "cph"),
|
||||
comment = "Javascript strftime library"),
|
||||
person(, "SpryMedia Limited", role = c("ctb", "cph"),
|
||||
comment = "DataTables library"),
|
||||
person("Ivan", "Sagalaev", role = c("ctb", "cph"),
|
||||
comment = "highlight.js library"),
|
||||
person("R Core Team", role = c("ctb", "cph"),
|
||||
comment = "tar implementation from R")
|
||||
)
|
||||
Description: Makes it incredibly easy to build interactive web
|
||||
applications with R. Automatic "reactive" binding between inputs and
|
||||
outputs and extensive prebuilt widgets make it possible to build
|
||||
beautiful, responsive, and powerful applications with minimal effort.
|
||||
License: GPL-3
|
||||
Depends: R (>= 2.14.1), methods, websockets (>= 1.1.4), caTools, RJSONIO, xtable
|
||||
Imports: stats, tools, utils, datasets
|
||||
URL: https://github.com/rstudio/shiny, http://rstudio.github.com/shiny/tutorial
|
||||
License: MIT + file LICENSE
|
||||
URL: https://shiny.posit.co/, https://github.com/rstudio/shiny
|
||||
BugReports: https://github.com/rstudio/shiny/issues
|
||||
Depends:
|
||||
methods,
|
||||
R (>= 3.0.2)
|
||||
Imports:
|
||||
bslib (>= 0.6.0),
|
||||
cachem (>= 1.1.0),
|
||||
cli,
|
||||
commonmark (>= 2.0.0),
|
||||
fastmap (>= 1.1.1),
|
||||
fontawesome (>= 0.4.0),
|
||||
glue (>= 1.3.2),
|
||||
grDevices,
|
||||
htmltools (>= 0.5.4),
|
||||
httpuv (>= 1.5.2),
|
||||
jsonlite (>= 0.9.16),
|
||||
later (>= 1.0.0),
|
||||
lifecycle (>= 0.2.0),
|
||||
mime (>= 0.3),
|
||||
otel,
|
||||
promises (>= 1.5.0),
|
||||
R6 (>= 2.0),
|
||||
rlang (>= 0.4.10),
|
||||
sourcetools,
|
||||
tools,
|
||||
utils,
|
||||
withr,
|
||||
xtable
|
||||
Suggests:
|
||||
Cairo (>= 1.5-5),
|
||||
coro (>= 1.1.0),
|
||||
datasets,
|
||||
DT,
|
||||
dygraphs,
|
||||
future,
|
||||
ggplot2,
|
||||
knitr (>= 1.6),
|
||||
magrittr,
|
||||
markdown,
|
||||
mirai,
|
||||
otelsdk (>= 0.2.0),
|
||||
ragg,
|
||||
reactlog (>= 1.0.0),
|
||||
rmarkdown,
|
||||
sass,
|
||||
showtext,
|
||||
testthat (>= 3.2.1),
|
||||
watcher,
|
||||
yaml
|
||||
Config/Needs/check: shinytest2
|
||||
Config/testthat/edition: 3
|
||||
Encoding: UTF-8
|
||||
Roxygen: list(markdown = TRUE)
|
||||
RoxygenNote: 7.3.3
|
||||
Collate:
|
||||
'globals.R'
|
||||
'app-state.R'
|
||||
'app_template.R'
|
||||
'bind-cache.R'
|
||||
'bind-event.R'
|
||||
'bookmark-state-local.R'
|
||||
'bookmark-state.R'
|
||||
'bootstrap-deprecated.R'
|
||||
'bootstrap-layout.R'
|
||||
'conditions.R'
|
||||
'map.R'
|
||||
'timer.R'
|
||||
'tags.R'
|
||||
'react.R'
|
||||
'reactives.R'
|
||||
'shiny.R'
|
||||
'shinywrappers.R'
|
||||
'shinyui.R'
|
||||
'slider.R'
|
||||
'utils.R'
|
||||
'bootstrap.R'
|
||||
'busy-indicators-spinners.R'
|
||||
'busy-indicators.R'
|
||||
'cache-utils.R'
|
||||
'deprecated.R'
|
||||
'devmode.R'
|
||||
'diagnose.R'
|
||||
'extended-task.R'
|
||||
'fileupload.R'
|
||||
'graph.R'
|
||||
'reactives.R'
|
||||
'reactive-domains.R'
|
||||
'history.R'
|
||||
'hooks.R'
|
||||
'html-deps.R'
|
||||
'image-interact-opts.R'
|
||||
'image-interact.R'
|
||||
'imageutils.R'
|
||||
'input-action.R'
|
||||
'input-checkbox.R'
|
||||
'input-checkboxgroup.R'
|
||||
'input-date.R'
|
||||
'input-daterange.R'
|
||||
'input-file.R'
|
||||
'input-numeric.R'
|
||||
'input-password.R'
|
||||
'input-radiobuttons.R'
|
||||
'input-select.R'
|
||||
'input-slider.R'
|
||||
'input-submit.R'
|
||||
'input-text.R'
|
||||
'input-textarea.R'
|
||||
'input-utils.R'
|
||||
'insert-tab.R'
|
||||
'insert-ui.R'
|
||||
'jqueryui.R'
|
||||
'knitr.R'
|
||||
'middleware-shiny.R'
|
||||
'middleware.R'
|
||||
'timer.R'
|
||||
'shiny.R'
|
||||
'mock-session.R'
|
||||
'modal.R'
|
||||
'modules.R'
|
||||
'notifications.R'
|
||||
'otel-attr-srcref.R'
|
||||
'otel-collect.R'
|
||||
'otel-enable.R'
|
||||
'otel-error.R'
|
||||
'otel-label.R'
|
||||
'otel-reactive-update.R'
|
||||
'otel-session.R'
|
||||
'otel-shiny.R'
|
||||
'otel-with.R'
|
||||
'priorityqueue.R'
|
||||
'progress.R'
|
||||
'react.R'
|
||||
'reexports.R'
|
||||
'render-cached-plot.R'
|
||||
'render-plot.R'
|
||||
'render-table.R'
|
||||
'run-url.R'
|
||||
'runapp.R'
|
||||
'serializers.R'
|
||||
'server-input-handlers.R'
|
||||
'server-resource-paths.R'
|
||||
'server.R'
|
||||
'shiny-options.R'
|
||||
'shiny-package.R'
|
||||
'shinyapp.R'
|
||||
'shinyui.R'
|
||||
'shinywrappers.R'
|
||||
'showcase.R'
|
||||
'snapshot.R'
|
||||
'staticimports.R'
|
||||
'tar.R'
|
||||
'test-export.R'
|
||||
'test-server.R'
|
||||
'test.R'
|
||||
'update-input.R'
|
||||
'utils-lang.R'
|
||||
'utils-tags.R'
|
||||
'version_bs_date_picker.R'
|
||||
'version_ion_range_slider.R'
|
||||
'version_jquery.R'
|
||||
'version_jqueryui.R'
|
||||
'version_selectize.R'
|
||||
'version_strftime.R'
|
||||
'viewer.R'
|
||||
|
||||
2
LICENSE
Normal file
2
LICENSE
Normal file
@@ -0,0 +1,2 @@
|
||||
YEAR: 2012-2025
|
||||
COPYRIGHT HOLDER: Posit Software, PBC
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# MIT License
|
||||
|
||||
Copyright (c) 2025 shiny authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
1011
LICENSE.note
Normal file
1011
LICENSE.note
Normal file
File diff suppressed because it is too large
Load Diff
399
NAMESPACE
399
NAMESPACE
@@ -1,12 +1,131 @@
|
||||
# Generated by roxygen2: do not edit by hand
|
||||
|
||||
S3method("$",mockclientdata)
|
||||
S3method("$",reactivevalues)
|
||||
S3method("$",session_proxy)
|
||||
S3method("$",shinyoutput)
|
||||
S3method("$<-",reactivevalues)
|
||||
S3method("$<-",session_proxy)
|
||||
S3method("$<-",shinyoutput)
|
||||
S3method("[",mockclientdata)
|
||||
S3method("[",reactivevalues)
|
||||
S3method("[",shinyoutput)
|
||||
S3method("[<-",reactivevalues)
|
||||
S3method("[<-",shinyoutput)
|
||||
S3method("[[",mockclientdata)
|
||||
S3method("[[",reactivevalues)
|
||||
S3method("[[",session_proxy)
|
||||
S3method("[[",shinyoutput)
|
||||
S3method("[[<-",reactivevalues)
|
||||
S3method("[[<-",shinyoutput)
|
||||
S3method("names<-",reactivevalues)
|
||||
S3method(as.list,Map)
|
||||
S3method(as.list,reactivevalues)
|
||||
S3method(as.shiny.appobj,character)
|
||||
S3method(as.shiny.appobj,list)
|
||||
S3method(as.shiny.appobj,shiny.appobj)
|
||||
S3method(as.tags,shiny.appobj)
|
||||
S3method(as.tags,shiny.render.function)
|
||||
S3method(bindCache,"function")
|
||||
S3method(bindCache,Observer)
|
||||
S3method(bindCache,default)
|
||||
S3method(bindCache,reactive.cache)
|
||||
S3method(bindCache,reactive.event)
|
||||
S3method(bindCache,reactiveExpr)
|
||||
S3method(bindCache,shiny.render.function)
|
||||
S3method(bindCache,shiny.render.function.cache)
|
||||
S3method(bindCache,shiny.render.function.event)
|
||||
S3method(bindCache,shiny.renderPlot)
|
||||
S3method(bindEvent,Observer)
|
||||
S3method(bindEvent,Observer.event)
|
||||
S3method(bindEvent,default)
|
||||
S3method(bindEvent,reactive.event)
|
||||
S3method(bindEvent,reactiveExpr)
|
||||
S3method(bindEvent,shiny.render.function)
|
||||
S3method(format,reactiveExpr)
|
||||
S3method(format,reactiveVal)
|
||||
S3method(length,Map)
|
||||
S3method(names,reactivevalues)
|
||||
S3method(print,reactive)
|
||||
S3method(print,reactivevalues)
|
||||
S3method(print,shiny.appobj)
|
||||
S3method(print,shiny.render.function)
|
||||
S3method(print,shiny_runtests)
|
||||
S3method(str,reactivevalues)
|
||||
export("conditionStackTrace<-")
|
||||
export(..stacktraceoff..)
|
||||
export(..stacktraceon..)
|
||||
export(ExtendedTask)
|
||||
export(HTML)
|
||||
export(MockShinySession)
|
||||
export(NS)
|
||||
export(Progress)
|
||||
export(a)
|
||||
export(absolutePanel)
|
||||
export(actionButton)
|
||||
export(actionLink)
|
||||
export(addResourcePath)
|
||||
export(animationOptions)
|
||||
export(appendTab)
|
||||
export(as.shiny.appobj)
|
||||
export(basicPage)
|
||||
export(bindCache)
|
||||
export(bindEvent)
|
||||
export(bookmarkButton)
|
||||
export(bootstrapLib)
|
||||
export(bootstrapPage)
|
||||
export(br)
|
||||
export(browserViewer)
|
||||
export(brushOpts)
|
||||
export(brushedPoints)
|
||||
export(busyIndicatorOptions)
|
||||
export(callModule)
|
||||
export(captureStackTraces)
|
||||
export(checkboxGroupInput)
|
||||
export(checkboxInput)
|
||||
export(clickOpts)
|
||||
export(code)
|
||||
export(column)
|
||||
export(conditionStackTrace)
|
||||
export(conditionalPanel)
|
||||
export(createRenderFunction)
|
||||
export(createWebDependency)
|
||||
export(dataTableOutput)
|
||||
export(dateInput)
|
||||
export(dateRangeInput)
|
||||
export(dblclickOpts)
|
||||
export(debounce)
|
||||
export(devmode)
|
||||
export(dialogViewer)
|
||||
export(diskCache)
|
||||
export(div)
|
||||
export(downloadButton)
|
||||
export(downloadHandler)
|
||||
export(downloadLink)
|
||||
export(em)
|
||||
export(enableBookmarking)
|
||||
export(eventReactive)
|
||||
export(exportTestValues)
|
||||
export(exprToFunction)
|
||||
export(fileInput)
|
||||
export(fillCol)
|
||||
export(fillPage)
|
||||
export(fillRow)
|
||||
export(fixedPage)
|
||||
export(fixedPanel)
|
||||
export(fixedRow)
|
||||
export(flowLayout)
|
||||
export(fluidPage)
|
||||
export(fluidRow)
|
||||
export(freezeReactiveVal)
|
||||
export(freezeReactiveValue)
|
||||
export(getCurrentOutputInfo)
|
||||
export(getCurrentTheme)
|
||||
export(getDefaultReactiveDomain)
|
||||
export(getQueryString)
|
||||
export(getShinyOption)
|
||||
export(getUrlHash)
|
||||
export(get_devmode_option)
|
||||
export(h1)
|
||||
export(h2)
|
||||
export(h3)
|
||||
@@ -15,49 +134,299 @@ export(h5)
|
||||
export(h6)
|
||||
export(headerPanel)
|
||||
export(helpText)
|
||||
export(HTML)
|
||||
export(hideTab)
|
||||
export(hoverOpts)
|
||||
export(hr)
|
||||
export(htmlOutput)
|
||||
export(htmlTemplate)
|
||||
export(httpResponse)
|
||||
export(icon)
|
||||
export(imageOutput)
|
||||
export(img)
|
||||
export(in_devmode)
|
||||
export(incProgress)
|
||||
export(includeCSS)
|
||||
export(includeHTML)
|
||||
export(includeMarkdown)
|
||||
export(includeScript)
|
||||
export(includeText)
|
||||
export(inputPanel)
|
||||
export(insertTab)
|
||||
export(insertUI)
|
||||
export(installExprFunction)
|
||||
export(invalidateLater)
|
||||
export(is.key_missing)
|
||||
export(is.reactive)
|
||||
export(is.reactivevalues)
|
||||
export(is.shiny.appobj)
|
||||
export(is.singleton)
|
||||
export(isRunning)
|
||||
export(isTruthy)
|
||||
export(isolate)
|
||||
export(key_missing)
|
||||
export(loadSupport)
|
||||
export(localOtelCollect)
|
||||
export(mainPanel)
|
||||
export(makeReactiveBinding)
|
||||
export(markRenderFunction)
|
||||
export(markdown)
|
||||
export(maskReactiveContext)
|
||||
export(memoryCache)
|
||||
export(modalButton)
|
||||
export(modalDialog)
|
||||
export(moduleServer)
|
||||
export(navbarMenu)
|
||||
export(navbarPage)
|
||||
export(navlistPanel)
|
||||
export(nearPoints)
|
||||
export(need)
|
||||
export(ns.sep)
|
||||
export(numericInput)
|
||||
export(observe)
|
||||
export(observeEvent)
|
||||
export(onBookmark)
|
||||
export(onBookmarked)
|
||||
export(onFlush)
|
||||
export(onFlushed)
|
||||
export(onReactiveDomainEnded)
|
||||
export(onRestore)
|
||||
export(onRestored)
|
||||
export(onSessionEnded)
|
||||
export(onStop)
|
||||
export(onUnhandledError)
|
||||
export(outputOptions)
|
||||
export(p)
|
||||
export(pageWithSidebar)
|
||||
export(paneViewer)
|
||||
export(parseQueryString)
|
||||
export(passwordInput)
|
||||
export(plotOutput)
|
||||
export(plotPNG)
|
||||
export(pre)
|
||||
export(prependTab)
|
||||
export(printError)
|
||||
export(printStackTrace)
|
||||
export(quoToFunction)
|
||||
export(radioButtons)
|
||||
export(reactive)
|
||||
export(reactivePlot)
|
||||
export(reactivePrint)
|
||||
export(reactiveTable)
|
||||
export(reactiveText)
|
||||
export(reactiveConsole)
|
||||
export(reactiveFileReader)
|
||||
export(reactivePoll)
|
||||
export(reactiveTimer)
|
||||
export(reactiveVal)
|
||||
export(reactiveValues)
|
||||
export(reactiveValuesToList)
|
||||
export(reactlog)
|
||||
export(reactlogAddMark)
|
||||
export(reactlogReset)
|
||||
export(reactlogShow)
|
||||
export(registerInputHandler)
|
||||
export(registerThemeDependency)
|
||||
export(register_devmode_option)
|
||||
export(removeInputHandler)
|
||||
export(removeModal)
|
||||
export(removeNotification)
|
||||
export(removeResourcePath)
|
||||
export(removeTab)
|
||||
export(removeUI)
|
||||
export(renderCachedPlot)
|
||||
export(renderDataTable)
|
||||
export(renderImage)
|
||||
export(renderPlot)
|
||||
export(renderPrint)
|
||||
export(renderTable)
|
||||
export(renderText)
|
||||
export(renderUI)
|
||||
export(repeatable)
|
||||
export(req)
|
||||
export(resourcePaths)
|
||||
export(restoreInput)
|
||||
export(runApp)
|
||||
export(runExample)
|
||||
export(runGadget)
|
||||
export(runGist)
|
||||
export(runGitHub)
|
||||
export(runTests)
|
||||
export(runUrl)
|
||||
export(safeError)
|
||||
export(selectInput)
|
||||
export(selectizeInput)
|
||||
export(serverInfo)
|
||||
export(setBookmarkExclude)
|
||||
export(setProgress)
|
||||
export(setSerializer)
|
||||
export(shinyApp)
|
||||
export(shinyAppDir)
|
||||
export(shinyAppFile)
|
||||
export(shinyAppTemplate)
|
||||
export(shinyOptions)
|
||||
export(shinyServer)
|
||||
export(shinyUI)
|
||||
export(showBookmarkUrlModal)
|
||||
export(showModal)
|
||||
export(showNotification)
|
||||
export(showTab)
|
||||
export(sidebarLayout)
|
||||
export(sidebarPanel)
|
||||
export(singleton)
|
||||
export(sizeGrowthRatio)
|
||||
export(sliderInput)
|
||||
export(snapshotExclude)
|
||||
export(snapshotPreprocessInput)
|
||||
export(snapshotPreprocessOutput)
|
||||
export(span)
|
||||
export(splitLayout)
|
||||
export(stopApp)
|
||||
export(strong)
|
||||
export(submitButton)
|
||||
export(tableOutput)
|
||||
export(suppressDependencies)
|
||||
export(tabPanel)
|
||||
export(tabPanelBody)
|
||||
export(tableOutput)
|
||||
export(tabsetPanel)
|
||||
export(tag)
|
||||
export(tagAppendAttributes)
|
||||
export(tagAppendChild)
|
||||
export(tagAppendChildren)
|
||||
export(tagGetAttribute)
|
||||
export(tagHasAttribute)
|
||||
export(tagList)
|
||||
export(tagSetChildren)
|
||||
export(tags)
|
||||
export(testServer)
|
||||
export(textAreaInput)
|
||||
export(textInput)
|
||||
export(textOutput)
|
||||
export(throttle)
|
||||
export(titlePanel)
|
||||
export(uiOutput)
|
||||
export(updateActionButton)
|
||||
export(updateActionLink)
|
||||
export(updateCheckboxGroupInput)
|
||||
export(updateCheckboxInput)
|
||||
export(updateDateInput)
|
||||
export(updateDateRangeInput)
|
||||
export(updateNavbarPage)
|
||||
export(updateNavlistPanel)
|
||||
export(updateNumericInput)
|
||||
export(updateQueryString)
|
||||
export(updateRadioButtons)
|
||||
export(updateSelectInput)
|
||||
export(updateSelectizeInput)
|
||||
export(updateSliderInput)
|
||||
export(updateTabsetPanel)
|
||||
export(updateTextAreaInput)
|
||||
export(updateTextInput)
|
||||
export(updateVarSelectInput)
|
||||
export(updateVarSelectizeInput)
|
||||
export(urlModal)
|
||||
export(useBusyIndicators)
|
||||
export(validate)
|
||||
export(validateCssUnit)
|
||||
export(varSelectInput)
|
||||
export(varSelectizeInput)
|
||||
export(verbatimTextOutput)
|
||||
S3method(as.character,shiny.tag)
|
||||
S3method(as.list,reactvaluesreader)
|
||||
S3method(format,shiny.tag)
|
||||
S3method(names,reactvaluesreader)
|
||||
S3method(print,shiny.tag)
|
||||
S3method(reactive,default)
|
||||
S3method(reactive,"function")
|
||||
S3method("$",reactvaluesreader)
|
||||
S3method("$<-",shinyoutput)
|
||||
export(verticalLayout)
|
||||
export(wellPanel)
|
||||
export(withLogErrors)
|
||||
export(withMathJax)
|
||||
export(withOtelCollect)
|
||||
export(withProgress)
|
||||
export(withReactiveDomain)
|
||||
export(withTags)
|
||||
export(with_devmode)
|
||||
import(R6)
|
||||
import(htmltools)
|
||||
import(httpuv)
|
||||
import(methods)
|
||||
import(mime)
|
||||
import(xtable)
|
||||
importFrom(fastmap,fastmap)
|
||||
importFrom(fastmap,is.key_missing)
|
||||
importFrom(fastmap,key_missing)
|
||||
importFrom(grDevices,dev.cur)
|
||||
importFrom(grDevices,dev.set)
|
||||
importFrom(htmltools,HTML)
|
||||
importFrom(htmltools,a)
|
||||
importFrom(htmltools,br)
|
||||
importFrom(htmltools,code)
|
||||
importFrom(htmltools,div)
|
||||
importFrom(htmltools,em)
|
||||
importFrom(htmltools,h1)
|
||||
importFrom(htmltools,h2)
|
||||
importFrom(htmltools,h3)
|
||||
importFrom(htmltools,h4)
|
||||
importFrom(htmltools,h5)
|
||||
importFrom(htmltools,h6)
|
||||
importFrom(htmltools,hr)
|
||||
importFrom(htmltools,htmlTemplate)
|
||||
importFrom(htmltools,img)
|
||||
importFrom(htmltools,includeCSS)
|
||||
importFrom(htmltools,includeHTML)
|
||||
importFrom(htmltools,includeMarkdown)
|
||||
importFrom(htmltools,includeScript)
|
||||
importFrom(htmltools,includeText)
|
||||
importFrom(htmltools,is.singleton)
|
||||
importFrom(htmltools,p)
|
||||
importFrom(htmltools,pre)
|
||||
importFrom(htmltools,singleton)
|
||||
importFrom(htmltools,span)
|
||||
importFrom(htmltools,strong)
|
||||
importFrom(htmltools,suppressDependencies)
|
||||
importFrom(htmltools,tag)
|
||||
importFrom(htmltools,tagAppendAttributes)
|
||||
importFrom(htmltools,tagAppendChild)
|
||||
importFrom(htmltools,tagAppendChildren)
|
||||
importFrom(htmltools,tagGetAttribute)
|
||||
importFrom(htmltools,tagHasAttribute)
|
||||
importFrom(htmltools,tagList)
|
||||
importFrom(htmltools,tagSetChildren)
|
||||
importFrom(htmltools,tags)
|
||||
importFrom(htmltools,validateCssUnit)
|
||||
importFrom(htmltools,withTags)
|
||||
importFrom(lifecycle,deprecated)
|
||||
importFrom(lifecycle,is_present)
|
||||
importFrom(promises,"%...!%")
|
||||
importFrom(promises,"%...>%")
|
||||
importFrom(promises,as.promise)
|
||||
importFrom(promises,hybrid_then)
|
||||
importFrom(promises,is.promise)
|
||||
importFrom(promises,is.promising)
|
||||
importFrom(promises,new_promise_domain)
|
||||
importFrom(promises,promise_reject)
|
||||
importFrom(promises,promise_resolve)
|
||||
importFrom(promises,with_promise_domain)
|
||||
importFrom(rlang,"%||%")
|
||||
importFrom(rlang,"fn_body<-")
|
||||
importFrom(rlang,"fn_fmls<-")
|
||||
importFrom(rlang,as_function)
|
||||
importFrom(rlang,as_quosure)
|
||||
importFrom(rlang,check_dots_empty)
|
||||
importFrom(rlang,check_dots_unnamed)
|
||||
importFrom(rlang,enexpr)
|
||||
importFrom(rlang,enquo)
|
||||
importFrom(rlang,enquo0)
|
||||
importFrom(rlang,enquos)
|
||||
importFrom(rlang,enquos0)
|
||||
importFrom(rlang,eval_tidy)
|
||||
importFrom(rlang,expr)
|
||||
importFrom(rlang,fn_body)
|
||||
importFrom(rlang,get_env)
|
||||
importFrom(rlang,get_expr)
|
||||
importFrom(rlang,inject)
|
||||
importFrom(rlang,is_false)
|
||||
importFrom(rlang,is_missing)
|
||||
importFrom(rlang,is_na)
|
||||
importFrom(rlang,is_quosure)
|
||||
importFrom(rlang,list2)
|
||||
importFrom(rlang,maybe_missing)
|
||||
importFrom(rlang,missing_arg)
|
||||
importFrom(rlang,new_function)
|
||||
importFrom(rlang,new_quosure)
|
||||
importFrom(rlang,pairlist2)
|
||||
importFrom(rlang,quo)
|
||||
importFrom(rlang,quo_get_expr)
|
||||
importFrom(rlang,quo_is_missing)
|
||||
importFrom(rlang,quo_set_env)
|
||||
importFrom(rlang,quo_set_expr)
|
||||
importFrom(rlang,zap_srcref)
|
||||
|
||||
30
NEWS
30
NEWS
@@ -1,30 +0,0 @@
|
||||
shiny 0.1.4
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Allow Bootstrap tabsets to act as reactive inputs; their value indicates which
|
||||
tab is active
|
||||
* Upgrade to Bootstrap 2.1
|
||||
* Add `checkboxGroupInput` control, which presents a list of checkboxes and
|
||||
returns a vector of the selected values
|
||||
* Add `addResourcePath`, intended for reusable component authors to access CSS,
|
||||
JavaScript, image files, etc. from their package directories
|
||||
* Add Shiny.bindInputs(scope), .unbindInputs(scope), .bindOutputs(scope), and
|
||||
.unbindOutputs(scope) JS API calls to allow dynamic binding/unbinding of HTML
|
||||
elements
|
||||
|
||||
|
||||
shiny 0.1.3
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* Introduce Shiny.inputBindings.register JS API and InputBinding class, for
|
||||
creating custom input controls
|
||||
* Add `step` parameter to numericInput
|
||||
* Read names of input using `names(input)`
|
||||
* Access snapshot of input as a list using `as.list(input)`
|
||||
* Fix issue #10: Plots in tabsets not rendered
|
||||
|
||||
|
||||
shiny 0.1.2
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Initial private beta release!
|
||||
26
R/app-state.R
Normal file
26
R/app-state.R
Normal file
@@ -0,0 +1,26 @@
|
||||
#' @include globals.R
|
||||
NULL
|
||||
|
||||
# The current app state is a place to read and hang state for the
|
||||
# currently-running application. This is useful for setting options that will
|
||||
# last as long as the application is running.
|
||||
|
||||
.globals$appState <- NULL
|
||||
|
||||
initCurrentAppState <- function(appobj) {
|
||||
if (!is.null(.globals$appState)) {
|
||||
stop("Can't initialize current app state when another is currently active.")
|
||||
}
|
||||
.globals$appState <- new.env(parent = emptyenv())
|
||||
.globals$appState$app <- appobj
|
||||
# Copy over global options
|
||||
.globals$appState$options <- .globals$options
|
||||
}
|
||||
|
||||
getCurrentAppState <- function() {
|
||||
.globals$appState
|
||||
}
|
||||
|
||||
clearCurrentAppState <- function() {
|
||||
.globals$appState <- NULL
|
||||
}
|
||||
281
R/app_template.R
Normal file
281
R/app_template.R
Normal file
@@ -0,0 +1,281 @@
|
||||
#' Generate a Shiny application from a template
|
||||
#'
|
||||
#' This function populates a directory with files for a Shiny application.
|
||||
#'
|
||||
#' In an interactive R session, this function will, by default, prompt the user
|
||||
#' to select which components to add to the application. Choices are
|
||||
#'
|
||||
#' ```
|
||||
#' 1: All
|
||||
#' 2: app.R : Main application file
|
||||
#' 3: R/example.R : Helper file with R code
|
||||
#' 4: R/example-module.R : Example module
|
||||
#' 5: tests/testthat/ : Tests using the testthat and shinytest2 package
|
||||
#' ```
|
||||
#'
|
||||
#' If option 1 is selected, the full example application including the
|
||||
#' following files and directories is created:
|
||||
#'
|
||||
#' ```
|
||||
#' appdir/
|
||||
#' |- app.R
|
||||
#' |- R
|
||||
#' | |- example-module.R
|
||||
#' | `- example.R
|
||||
#' `- tests
|
||||
#' |- testthat.R
|
||||
#' `- testthat
|
||||
#' |- setup-shinytest2.R
|
||||
#' |- test-examplemodule.R
|
||||
#' |- test-server.R
|
||||
#' |- test-shinytest2.R
|
||||
#' `- test-sort.R
|
||||
#' ```
|
||||
#'
|
||||
#' Some notes about these files:
|
||||
#' * `app.R` is the main application file.
|
||||
#' * All files in the `R/` subdirectory are automatically sourced when the
|
||||
#' application is run.
|
||||
#' * `R/example.R` and `R/example-module.R` are automatically sourced when
|
||||
#' the application is run. The first contains a function `lexical_sort()`,
|
||||
#' and the second contains code for module created by the
|
||||
#' [moduleServer()] function, which is used in the application.
|
||||
#' * `tests/` contains various tests for the application. You may
|
||||
#' choose to use or remove any of them. They can be executed by the
|
||||
#' [runTests()] function.
|
||||
#' * `tests/testthat.R` is a test runner for test files in the
|
||||
#' `tests/testthat/` directory using the
|
||||
#' [shinytest2](https://rstudio.github.io/shinytest2/reference/test_app.html)
|
||||
#' package.
|
||||
#' * `tests/testthat/setup-shinytest2.R` is setup file to source your `./R` folder into the testing environment.
|
||||
#' * `tests/testthat/test-examplemodule.R` is a test for an application's module server function.
|
||||
#' * `tests/testthat/test-server.R` is a test for the application's server code
|
||||
#' * `tests/testthat/test-shinytest2.R` is a test that uses the
|
||||
#' [shinytest2](https://rstudio.github.io/shinytest2/) package to do
|
||||
#' snapshot-based testing.
|
||||
#' * `tests/testthat/test-sort.R` is a test for a supporting function in the `R/` directory.
|
||||
#'
|
||||
#' @param path Path to create new shiny application template.
|
||||
#' @param examples Either one of "default", "ask", "all", or any combination of
|
||||
#' "app", "rdir", "module", and "tests". In an
|
||||
#' interactive session, "default" falls back to "ask"; in a non-interactive
|
||||
#' session, "default" falls back to "all". With "ask", this function will
|
||||
#' prompt the user to select which template items will be added to the new app
|
||||
#' directory. With "all", all template items will be added to the app
|
||||
#' directory.
|
||||
#' @param dryrun If `TRUE`, don't actually write any files; just print out which
|
||||
#' files would be written.
|
||||
#'
|
||||
#' @export
|
||||
shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
|
||||
{
|
||||
if (is.null(path)) {
|
||||
stop("Please provide a `path`.")
|
||||
}
|
||||
|
||||
# =======================================================
|
||||
# Option handling
|
||||
# =======================================================
|
||||
|
||||
choices <- c(
|
||||
app = "app.R : Main application file",
|
||||
rdir = "R/example.R : Helper file with R code",
|
||||
module = "R/example-module.R : Example module",
|
||||
tests = "tests/testthat/ : Tests using {testthat} and {shinytest2}"
|
||||
)
|
||||
|
||||
# Support legacy value
|
||||
examples[examples == "shinytest"] <- "tests"
|
||||
examples[examples == "testthat"] <- "tests"
|
||||
examples <- unique(examples)
|
||||
|
||||
if (identical(examples, "default")) {
|
||||
if (rlang::is_interactive()) {
|
||||
examples <- "ask"
|
||||
} else {
|
||||
examples <- "all"
|
||||
}
|
||||
}
|
||||
|
||||
if (!identical(examples, "ask") &&
|
||||
!identical(examples, "all") &&
|
||||
any(! examples %in% names(choices)))
|
||||
{
|
||||
stop('`examples` must be one of "default", "ask", "all", or any combination of "',
|
||||
paste(names(choices), collapse = '", "'), '".')
|
||||
}
|
||||
|
||||
if (identical(examples, "ask")) {
|
||||
response <- select_menu(
|
||||
c(all = "All", choices),
|
||||
title = paste0(
|
||||
"Select which of the following to add at ", path, "/ :"
|
||||
),
|
||||
msg = "Enter one or more numbers (with spaces), or an empty line to exit: \n"
|
||||
)
|
||||
|
||||
examples <- names(response)
|
||||
}
|
||||
|
||||
examples <- unique(examples)
|
||||
|
||||
if ("all" %in% examples) {
|
||||
examples <- names(choices)
|
||||
}
|
||||
|
||||
if (length(examples) == 0) {
|
||||
return(invisible())
|
||||
}
|
||||
|
||||
if ("tests" %in% examples) {
|
||||
rlang::check_installed("shinytest2", "for {testthat} tests to work as expected", version = "0.2.0")
|
||||
}
|
||||
|
||||
# =======================================================
|
||||
# Utility functions
|
||||
# =======================================================
|
||||
|
||||
# Check if a directory is empty, ignoring certain files
|
||||
dir_is_empty <- function(path) {
|
||||
files <- list.files(path, all.files = TRUE, no.. = TRUE)
|
||||
# Ignore .DS_Store files, which are sometimes automatically created on macOS
|
||||
files <- setdiff(files, ".DS_Store")
|
||||
return(length(files) != 0)
|
||||
}
|
||||
|
||||
# Helper to resolve paths relative to our template
|
||||
template_path <- function(...) {
|
||||
system_file("app_template", ..., package = "shiny")
|
||||
}
|
||||
|
||||
# Resolve path relative to destination
|
||||
dest_path <- function(...) {
|
||||
file.path(path, ...)
|
||||
}
|
||||
|
||||
mkdir <- function(path) {
|
||||
if (!dirExists(path)) {
|
||||
message("Creating ", ensure_trailing_slash(path))
|
||||
if (!dryrun) {
|
||||
dir.create(path, recursive = TRUE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Copy a file from the template directory to the destination directory. If the
|
||||
# file has templating code (it contains `{{` in the text), then run it through
|
||||
# the htmlTemplate().
|
||||
copy_file_one <- function(name) {
|
||||
from <- template_path(name)
|
||||
to <- dest_path(name)
|
||||
|
||||
message("Creating ", to)
|
||||
if (file.exists(to)) {
|
||||
stop(to, " already exists. Please remove it and try again.", call. = FALSE)
|
||||
}
|
||||
|
||||
if (!dryrun) {
|
||||
is_template <- any(grepl("{{", readLines(from), fixed = TRUE))
|
||||
|
||||
if (is_template) {
|
||||
writeChar(
|
||||
as.character(htmlTemplate(
|
||||
from,
|
||||
rdir = "rdir" %in% examples,
|
||||
module = "module" %in% examples
|
||||
)),
|
||||
con = to,
|
||||
eos = NULL
|
||||
)
|
||||
} else {
|
||||
file.copy(from, to)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Copy multiple files from template to destination.
|
||||
copy_file <- function(names) {
|
||||
for (name in names) {
|
||||
copy_file_one(name)
|
||||
}
|
||||
}
|
||||
|
||||
# Copy the files for a tests/ subdirectory
|
||||
copy_test_dir <- function() {
|
||||
files <- dir(template_path("tests"), recursive = TRUE)
|
||||
|
||||
# Filter out files that are not module files in the R directory.
|
||||
if (! "rdir" %in% examples) {
|
||||
# find all files in the testthat folder that are not module or server files
|
||||
is_r_folder_file <- !grepl("module|server|shinytest2|testthat", basename(files))
|
||||
files <- files[!is_r_folder_file]
|
||||
}
|
||||
|
||||
# Filter out module files, if applicable.
|
||||
if (! "module" %in% examples) {
|
||||
files <- files[!grepl("module", files)]
|
||||
}
|
||||
|
||||
mkdir(dest_path("tests"))
|
||||
|
||||
# Create any subdirectories if needed
|
||||
dirs <- setdiff(unique(dirname(files)), ".")
|
||||
for (dir in dirs) {
|
||||
mkdir(dest_path("tests", dir))
|
||||
}
|
||||
|
||||
copy_file(file.path("tests", files))
|
||||
}
|
||||
|
||||
# =======================================================
|
||||
# Main function
|
||||
# =======================================================
|
||||
|
||||
if (is.null(path)) {
|
||||
stop("`path` is missing.")
|
||||
}
|
||||
if (file.exists(path) && !dirExists(path)) {
|
||||
stop(path, " exists but is not a directory.")
|
||||
}
|
||||
|
||||
if (dirExists(path) && dir_is_empty(path)) {
|
||||
if (interactive()) {
|
||||
response <- readline(paste0(
|
||||
ensure_trailing_slash(path),
|
||||
" is not empty. Do you want to use this directory anyway? [y/n] "
|
||||
))
|
||||
if (tolower(response) != "y") {
|
||||
return(invisible())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mkdir(path)
|
||||
}
|
||||
|
||||
if ("app" %in% examples) {
|
||||
copy_file("app.R")
|
||||
}
|
||||
|
||||
# R/ dir with non-module files
|
||||
if ("rdir" %in% examples) {
|
||||
files <- dir(template_path("R"))
|
||||
non_module_files <- files[!grepl("module.R$", files)]
|
||||
mkdir(dest_path("R"))
|
||||
copy_file(file.path("R", non_module_files))
|
||||
}
|
||||
|
||||
# R/ dir with module files
|
||||
if ("module" %in% examples) {
|
||||
files <- dir(template_path("R"))
|
||||
module_files <- files[grepl("module.R$", files)]
|
||||
mkdir(dest_path("R"))
|
||||
copy_file(file.path("R", module_files))
|
||||
}
|
||||
|
||||
# tests/testthat dir
|
||||
if ("tests" %in% examples) {
|
||||
copy_test_dir()
|
||||
}
|
||||
|
||||
invisible()
|
||||
}
|
||||
793
R/bind-cache.R
Normal file
793
R/bind-cache.R
Normal file
@@ -0,0 +1,793 @@
|
||||
utils::globalVariables(".GenericCallEnv", add = TRUE)
|
||||
|
||||
#' Add caching with reactivity to an object
|
||||
#'
|
||||
#' @description
|
||||
#'
|
||||
#' `bindCache()` adds caching [reactive()] expressions and `render*` functions
|
||||
#' (like [renderText()], [renderTable()], ...).
|
||||
#'
|
||||
#' Ordinary [reactive()] expressions automatically cache their _most recent_
|
||||
#' value, which helps to avoid redundant computation in downstream reactives.
|
||||
#' `bindCache()` will cache all previous values (as long as they fit in the
|
||||
#' cache) and they can be shared across user sessions. This allows
|
||||
#' `bindCache()` to dramatically improve performance when used correctly.
|
||||
|
||||
#' @details
|
||||
#'
|
||||
#' `bindCache()` requires one or more expressions that are used to generate a
|
||||
#' **cache key**, which is used to determine if a computation has occurred
|
||||
#' before and hence can be retrieved from the cache. If you're familiar with the
|
||||
#' concept of memoizing pure functions (e.g., the \pkg{memoise} package), you
|
||||
#' can think of the cache key as the input(s) to a pure function. As such, one
|
||||
#' should take care to make sure the use of `bindCache()` is _pure_ in the same
|
||||
#' sense, namely:
|
||||
#'
|
||||
#' 1. For a given key, the return value is always the same.
|
||||
#' 2. Evaluation has no side-effects.
|
||||
#'
|
||||
#' In the example here, the `bindCache()` key consists of `input$x` and
|
||||
#' `input$y` combined, and the value is `input$x * input$y`. In this simple
|
||||
#' example, for any given key, there is only one possible returned value.
|
||||
#'
|
||||
#' ```
|
||||
#' r <- reactive({ input$x * input$y }) %>%
|
||||
#' bindCache(input$x, input$y)
|
||||
#' ```
|
||||
#'
|
||||
|
||||
#' The largest performance improvements occur when the cache key is fast to
|
||||
#' compute and the reactive expression is slow to compute. To see if the value
|
||||
#' should be computed, a cached reactive evaluates the key, and then serializes
|
||||
#' and hashes the result. If the resulting hashed key is in the cache, then the
|
||||
#' cached reactive simply retrieves the previously calculated value and returns
|
||||
#' it; if not, then the value is computed and the result is stored in the cache
|
||||
#' before being returned.
|
||||
#'
|
||||
#' To compute the cache key, `bindCache()` hashes the contents of `...`, so it's
|
||||
#' best to avoid including large objects in a cache key since that can result in
|
||||
#' slow hashing. It's also best to avoid reference objects like environments and
|
||||
#' R6 objects, since the serialization of these objects may not capture relevant
|
||||
#' changes.
|
||||
#'
|
||||
#' If you want to use a large object as part of a cache key, it may make sense
|
||||
#' to do some sort of reduction on the data that still captures information
|
||||
#' about whether a value can be retrieved from the cache. For example, if you
|
||||
#' have a large data set with timestamps, it might make sense to extract the
|
||||
#' most recent timestamp and return that. Then, instead of hashing the entire
|
||||
#' data object, the cached reactive only needs to hash the timestamp.
|
||||
#'
|
||||
#' ```
|
||||
#' r <- reactive({ compute(bigdata()) } %>%
|
||||
#' bindCache({ extract_most_recent_time(bigdata()) })
|
||||
#' ```
|
||||
#'
|
||||
#' For computations that are very slow, it often makes sense to pair
|
||||
#' [bindCache()] with [bindEvent()] so that no computation is performed until
|
||||
#' the user explicitly requests it (for more, see the Details section of
|
||||
#' [bindEvent()]).
|
||||
|
||||
#' @section Cache keys and reactivity:
|
||||
#'
|
||||
#' Because the **value** expression (from the original [reactive()]) is
|
||||
#' cached, it is not necessarily re-executed when someone retrieves a value,
|
||||
#' and therefore it can't be used to decide what objects to take reactive
|
||||
#' dependencies on. Instead, the **key** is used to figure out which objects
|
||||
#' to take reactive dependencies on. In short, the key expression is reactive,
|
||||
#' and value expression is no longer reactive.
|
||||
#'
|
||||
#' Here's an example of what not to do: if the key is `input$x` and the value
|
||||
#' expression is from `reactive({input$x + input$y})`, then the resulting
|
||||
#' cached reactive will only take a reactive dependency on `input$x` -- it
|
||||
#' won't recompute `{input$x + input$y}` when just `input$y` changes.
|
||||
#' Moreover, the cache won't use `input$y` as part of the key, and so it could
|
||||
#' return incorrect values in the future when it retrieves values from the
|
||||
#' cache. (See the examples below for an example of this.)
|
||||
#'
|
||||
#' A better cache key would be something like `input$x, input$y`. This does
|
||||
#' two things: it ensures that a reactive dependency is taken on both
|
||||
#' `input$x` and `input$y`, and it also makes sure that both values are
|
||||
#' represented in the cache key.
|
||||
#'
|
||||
#' In general, `key` should use the same reactive inputs as `value`, but the
|
||||
#' computation should be simpler. If there are other (non-reactive) values
|
||||
#' that are consumed, such as external data sources, they should be used in
|
||||
#' the `key` as well. Note that if the `key` is large, it can make sense to do
|
||||
#' some sort of reduction on it so that the serialization and hashing of the
|
||||
#' cache key is not too expensive.
|
||||
#'
|
||||
#' Remember that the key is _reactive_, so it is not re-executed every single
|
||||
#' time that someone accesses the cached reactive. It is only re-executed if
|
||||
#' it has been invalidated by one of the reactives it depends on. For
|
||||
#' example, suppose we have this cached reactive:
|
||||
#'
|
||||
#' ```
|
||||
#' r <- reactive({ input$x * input$y }) %>%
|
||||
#' bindCache(input$x, input$y)
|
||||
#' ```
|
||||
#'
|
||||
#' In this case, the key expression is essentially `reactive(list(input$x,
|
||||
#' input$y))` (there's a bit more to it, but that's a good enough
|
||||
#' approximation). The first time `r()` is called, it executes the key, then
|
||||
#' fails to find it in the cache, so it executes the value expression, `{
|
||||
#' input$x + input$y }`. If `r()` is called again, then it does not need to
|
||||
#' re-execute the key expression, because it has not been invalidated via a
|
||||
#' change to `input$x` or `input$y`; it simply returns the previous value.
|
||||
#' However, if `input$x` or `input$y` changes, then the reactive expression will
|
||||
#' be invalidated, and the next time that someone calls `r()`, the key
|
||||
#' expression will need to be re-executed.
|
||||
#'
|
||||
#' Note that if the cached reactive is passed to [bindEvent()], then the key
|
||||
#' expression will no longer be reactive; instead, the event expression will be
|
||||
#' reactive.
|
||||
#'
|
||||
#'
|
||||
#' @section Cache scope:
|
||||
#'
|
||||
#' By default, when `bindCache()` is used, it is scoped to the running
|
||||
#' application. That means that it shares a cache with all user sessions
|
||||
#' connected to the application (within the R process). This is done with the
|
||||
#' `cache` parameter's default value, `"app"`.
|
||||
#'
|
||||
#' With an app-level cache scope, one user can benefit from the work done for
|
||||
#' another user's session. In most cases, this is the best way to get
|
||||
#' performance improvements from caching. However, in some cases, this could
|
||||
#' leak information between sessions. For example, if the cache key does not
|
||||
#' fully encompass the inputs used by the value, then data could leak between
|
||||
#' the sessions. Or if a user sees that a cached reactive returns its value
|
||||
#' very quickly, they may be able to infer that someone else has already used
|
||||
#' it with the same values.
|
||||
#'
|
||||
#' It is also possible to scope the cache to the session, with
|
||||
#' `cache="session"`. This removes the risk of information leaking between
|
||||
#' sessions, but then one session cannot benefit from computations performed in
|
||||
#' another session.
|
||||
#'
|
||||
#' It is possible to pass in caching objects directly to
|
||||
#' `bindCache()`. This can be useful if, for example, you want to use a
|
||||
#' particular type of cache with specific cached reactives, or if you want to
|
||||
#' use a [cachem::cache_disk()] that is shared across multiple processes and
|
||||
#' persists beyond the current R session.
|
||||
#'
|
||||
#' To use different settings for an application-scoped cache, you can call
|
||||
#' [shinyOptions()] at the top of your app.R, server.R, or
|
||||
#' global.R. For example, this will create a cache with 500 MB of space
|
||||
#' instead of the default 200 MB:
|
||||
#'
|
||||
#' ```
|
||||
#' shinyOptions(cache = cachem::cache_mem(max_size = 500e6))
|
||||
#' ```
|
||||
#'
|
||||
#' To use different settings for a session-scoped cache, you can set
|
||||
#' `session$cache` at the top of your server function. By default, it will
|
||||
#' create a 200 MB memory cache for each session, but you can replace it with
|
||||
#' something different. To use the session-scoped cache, you must also call
|
||||
#' `bindCache()` with `cache="session"`. This will create a 100 MB cache for
|
||||
#' the session:
|
||||
#'
|
||||
#' ```
|
||||
#' function(input, output, session) {
|
||||
#' session$cache <- cachem::cache_mem(max_size = 100e6)
|
||||
#' ...
|
||||
#' }
|
||||
#' ```
|
||||
#'
|
||||
#' If you want to use a cache that is shared across multiple R processes, you
|
||||
#' can use a [cachem::cache_disk()]. You can create a application-level shared
|
||||
#' cache by putting this at the top of your app.R, server.R, or global.R:
|
||||
#'
|
||||
#' ```
|
||||
#' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache")))
|
||||
#' ```
|
||||
#'
|
||||
#' This will create a subdirectory in your system temp directory named
|
||||
#' `myapp-cache` (replace `myapp-cache` with a unique name of
|
||||
#' your choosing). On most platforms, this directory will be removed when
|
||||
#' your system reboots. This cache will persist across multiple starts and
|
||||
#' stops of the R process, as long as you do not reboot.
|
||||
#'
|
||||
#' To have the cache persist even across multiple reboots, you can create the
|
||||
#' cache in a location outside of the temp directory. For example, it could
|
||||
#' be a subdirectory of the application:
|
||||
#'
|
||||
#' ```
|
||||
#' shinyOptions(cache = cachem::cache_disk("./myapp-cache"))
|
||||
#' ```
|
||||
#'
|
||||
#' In this case, resetting the cache will have to be done manually, by deleting
|
||||
#' the directory.
|
||||
#'
|
||||
#' You can also scope a cache to just one item, or selected items. To do that,
|
||||
#' create a [cachem::cache_mem()] or [cachem::cache_disk()], and pass it
|
||||
#' as the `cache` argument of `bindCache()`.
|
||||
#'
|
||||
|
||||
#'
|
||||
#' @section Computing cache keys:
|
||||
#'
|
||||
#' The actual cache key that is used internally takes value from evaluating
|
||||
#' the key expression(s) (from the `...` arguments) and combines it with the
|
||||
#' (unevaluated) value expression.
|
||||
#'
|
||||
#' This means that if there are two cached reactives which have the same
|
||||
#' result from evaluating the key, but different value expressions, then they
|
||||
#' will not need to worry about collisions.
|
||||
#'
|
||||
#' However, if two cached reactives have identical key and value expressions
|
||||
#' expressions, they will share the cached values. This is useful when using
|
||||
#' `cache="app"`: there may be multiple user sessions which create separate
|
||||
#' cached reactive objects (because they are created from the same code in the
|
||||
#' server function, but the server function is executed once for each user
|
||||
#' session), and those cached reactive objects across sessions can share
|
||||
#' values in the cache.
|
||||
|
||||
|
||||
|
||||
#'
|
||||
#' @section Async with cached reactives:
|
||||
#'
|
||||
#' With a cached reactive expression, the key and/or value expression can be
|
||||
#' _asynchronous_. In other words, they can be promises --- not regular R
|
||||
#' promises, but rather objects provided by the
|
||||
#' \href{https://rstudio.github.io/promises/}{\pkg{promises}} package, which
|
||||
#' are similar to promises in JavaScript. (See [promises::promise()] for more
|
||||
#' information.) You can also use [mirai::mirai()] or [future::future()]
|
||||
#' objects to run code in a separate process or even on a remote machine.
|
||||
#'
|
||||
#' If the value returns a promise, then anything that consumes the cached
|
||||
#' reactive must expect it to return a promise.
|
||||
#'
|
||||
#' Similarly, if the key is a promise (in other words, if it is asynchronous),
|
||||
#' then the entire cached reactive must be asynchronous, since the key must be
|
||||
#' computed asynchronously before it knows whether to compute the value or the
|
||||
#' value is retrieved from the cache. Anything that consumes the cached
|
||||
#' reactive must therefore expect it to return a promise.
|
||||
#'
|
||||
|
||||
#'
|
||||
#' @section Developing render functions for caching:
|
||||
#'
|
||||
#' If you've implemented your own `render*()` function, it may just work with
|
||||
#' `bindCache()`, but it is possible that you will need to make some
|
||||
#' modifications. These modifications involve helping `bindCache()` avoid
|
||||
#' cache collisions, dealing with internal state that may be set by the,
|
||||
#' `render` function, and modifying the data as it goes in and comes out of
|
||||
#' the cache.
|
||||
#'
|
||||
#' You may need to provide a `cacheHint` to [createRenderFunction()] (or
|
||||
#' `htmlwidgets::shinyRenderWidget()`, if you've authored an htmlwidget) in
|
||||
#' order for `bindCache()` to correctly compute a cache key.
|
||||
#'
|
||||
#' The potential problem is a cache collision. Consider the following:
|
||||
#'
|
||||
#' ```
|
||||
#' output$x1 <- renderText({ input$x }) %>% bindCache(input$x)
|
||||
#' output$x2 <- renderText({ input$x * 2 }) %>% bindCache(input$x)
|
||||
#' ```
|
||||
#'
|
||||
#' Both `output$x1` and `output$x2` use `input$x` as part of their cache key,
|
||||
#' but if it were the only thing used in the cache key, then the two outputs
|
||||
#' would have a cache collision, and they would have the same output. To avoid
|
||||
#' this, a _cache hint_ is automatically added when [renderText()] calls
|
||||
#' [createRenderFunction()]. The cache hint is used as part of the actual
|
||||
#' cache key, in addition to the one passed to `bindCache()` by the user. The
|
||||
#' cache hint can be viewed by calling the internal Shiny function
|
||||
#' `extractCacheHint()`:
|
||||
#'
|
||||
#' ```
|
||||
#' r <- renderText({ input$x })
|
||||
#' shiny:::extractCacheHint(r)
|
||||
#' ```
|
||||
#'
|
||||
#' This returns a nested list containing an item, `$origUserFunc$body`, which
|
||||
#' in this case is the expression which was passed to `renderText()`:
|
||||
#' `{ input$x }`. This (quoted) expression is mixed into the actual cache
|
||||
#' key, and it is how `output$x1` does not have collisions with `output$x2`.
|
||||
#'
|
||||
#' For most developers of render functions, nothing extra needs to be done;
|
||||
#' the automatic inference of the cache hint is sufficient. Again, you can
|
||||
#' check it by calling `shiny:::extractCacheHint()`, and by testing the
|
||||
#' render function for cache collisions in a real application.
|
||||
#'
|
||||
#' In some cases, however, the automatic cache hint inference is not
|
||||
#' sufficient, and it is necessary to provide a cache hint. This is true
|
||||
#' for `renderPrint()`. Unlike `renderText()`, it wraps the user-provided
|
||||
#' expression in another function, before passing it to [createRenderFunction()]
|
||||
#' (instead of [createRenderFunction()]). Because the user code is wrapped in
|
||||
#' another function, `createRenderFunction()` is not able to automatically
|
||||
#' extract the user-provided code and use it in the cache key. Instead,
|
||||
#' `renderPrint` calls `createRenderFunction()`, it explicitly passes along a
|
||||
#' `cacheHint`, which includes a label and the original user expression.
|
||||
#'
|
||||
#' In general, if you need to provide a `cacheHint`, it is best practice to
|
||||
#' provide a `label` id, the user's `expr`, as well as any other arguments
|
||||
#' that may influence the final value.
|
||||
#'
|
||||
#' For \pkg{htmlwidgets}, it will try to automatically infer a cache hint;
|
||||
#' again, you can inspect the cache hint with `shiny:::extractCacheHint()` and
|
||||
#' also test it in an application. If you do need to explicitly provide a
|
||||
#' cache hint, pass it to `shinyRenderWidget`. For example:
|
||||
#'
|
||||
#' ```
|
||||
#' renderMyWidget <- function(expr) {
|
||||
#' q <- rlang::enquo0(expr)
|
||||
#'
|
||||
#' htmlwidgets::shinyRenderWidget(
|
||||
#' q,
|
||||
#' myWidgetOutput,
|
||||
#' quoted = TRUE,
|
||||
#' cacheHint = list(label = "myWidget", userQuo = q)
|
||||
#' )
|
||||
#' }
|
||||
#' ```
|
||||
#'
|
||||
#' If your `render` function sets any internal state, you may find it useful
|
||||
#' in your call to [createRenderFunction()] to use
|
||||
#' the `cacheWriteHook` and/or `cacheReadHook` parameters. These hooks are
|
||||
#' functions that run just before the object is stored in the cache, and just
|
||||
#' after the object is retrieved from the cache. They can modify the data
|
||||
#' that is stored and retrieved; this can be useful if extra information needs
|
||||
#' to be stored in the cache. They can also be used to modify the state of the
|
||||
#' application; for example, it can call [createWebDependency()] to make
|
||||
#' JS/CSS resources available if the cached object is loaded in a different R
|
||||
#' process. (See the source of `htmlwidgets::shinyRenderWidget` for an example
|
||||
#' of this.)
|
||||
#'
|
||||
#' @section Uncacheable objects:
|
||||
#'
|
||||
#' Some render functions cannot be cached, typically because they have side
|
||||
#' effects or modify some external state, and they must re-execute each time
|
||||
#' in order to work properly.
|
||||
#'
|
||||
#' For developers of such code, they should call [createRenderFunction()] (or
|
||||
#' [markRenderFunction()]) with `cacheHint = FALSE`.
|
||||
#'
|
||||
#'
|
||||
#' @section Caching with `renderPlot()`:
|
||||
#'
|
||||
#' When `bindCache()` is used with `renderPlot()`, the `height` and `width`
|
||||
#' passed to the original `renderPlot()` are ignored. They are superseded by
|
||||
#' `sizePolicy` argument passed to `bindCache. The default is:
|
||||
#'
|
||||
#' ```
|
||||
#' sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2)
|
||||
#' ```
|
||||
#'
|
||||
#' `sizePolicy` must be a function that takes a two-element numeric vector as
|
||||
#' input, representing the width and height of the `<img>` element in the
|
||||
#' browser window, and it must return a two-element numeric vector, representing
|
||||
#' the pixel dimensions of the plot to generate. The purpose is to round the
|
||||
#' actual pixel dimensions from the browser to some other dimensions, so that
|
||||
#' this will not generate and cache images of every possible pixel dimension.
|
||||
#' See [sizeGrowthRatio()] for more information on the default sizing policy.
|
||||
#'
|
||||
#' @param x The object to add caching to.
|
||||
#' @param ... One or more expressions to use in the caching key.
|
||||
#' @param cache The scope of the cache, or a cache object. This can be `"app"`
|
||||
#' (the default), `"session"`, or a cache object like a
|
||||
#' [cachem::cache_disk()]. See the Cache Scoping section for more information.
|
||||
#'
|
||||
#' @seealso [bindEvent()], [renderCachedPlot()] for caching plots.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' rc <- bindCache(
|
||||
#' x = reactive({
|
||||
#' Sys.sleep(2) # Pretend this is expensive
|
||||
#' input$x * 100
|
||||
#' }),
|
||||
#' input$x
|
||||
#' )
|
||||
#'
|
||||
#' # Can make it prettier with the %>% operator
|
||||
#' library(magrittr)
|
||||
#'
|
||||
#' rc <- reactive({
|
||||
#' Sys.sleep(2)
|
||||
#' input$x * 100
|
||||
#' }) %>%
|
||||
#' bindCache(input$x)
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' ## Only run app examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # Basic example
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' sliderInput("x", "x", 1, 10, 5),
|
||||
#' sliderInput("y", "y", 1, 10, 5),
|
||||
#' div("x * y: "),
|
||||
#' verbatimTextOutput("txt")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' r <- reactive({
|
||||
#' # The value expression is an _expensive_ computation
|
||||
#' message("Doing expensive computation...")
|
||||
#' Sys.sleep(2)
|
||||
#' input$x * input$y
|
||||
#' }) %>%
|
||||
#' bindCache(input$x, input$y)
|
||||
#'
|
||||
#' output$txt <- renderText(r())
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # Caching renderText
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' sliderInput("x", "x", 1, 10, 5),
|
||||
#' sliderInput("y", "y", 1, 10, 5),
|
||||
#' div("x * y: "),
|
||||
#' verbatimTextOutput("txt")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$txt <- renderText({
|
||||
#' message("Doing expensive computation...")
|
||||
#' Sys.sleep(2)
|
||||
#' input$x * input$y
|
||||
#' }) %>%
|
||||
#' bindCache(input$x, input$y)
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # Demo of using events and caching with an actionButton
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' sliderInput("x", "x", 1, 10, 5),
|
||||
#' sliderInput("y", "y", 1, 10, 5),
|
||||
#' actionButton("go", "Go"),
|
||||
#' div("x * y: "),
|
||||
#' verbatimTextOutput("txt")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' r <- reactive({
|
||||
#' message("Doing expensive computation...")
|
||||
#' Sys.sleep(2)
|
||||
#' input$x * input$y
|
||||
#' }) %>%
|
||||
#' bindCache(input$x, input$y) %>%
|
||||
#' bindEvent(input$go)
|
||||
#' # The cached, eventified reactive takes a reactive dependency on
|
||||
#' # input$go, but doesn't use it for the cache key. It uses input$x and
|
||||
#' # input$y for the cache key, but doesn't take a reactive dependency on
|
||||
#' # them, because the reactive dependency is superseded by addEvent().
|
||||
#'
|
||||
#' output$txt <- renderText(r())
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' @export
|
||||
bindCache <- function(x, ..., cache = "app") {
|
||||
force(cache)
|
||||
|
||||
UseMethod("bindCache")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.default <- function(x, ...) {
|
||||
stop("Don't know how to handle object with class ", paste(class(x), collapse = ", "))
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.reactiveExpr <- function(x, ..., cache = "app") {
|
||||
check_dots_unnamed()
|
||||
|
||||
call_srcref <- get_call_srcref(-1)
|
||||
label <- rassignSrcrefToLabel(
|
||||
call_srcref,
|
||||
defaultLabel = exprToLabel(substitute(x), "cachedReactive")
|
||||
)
|
||||
|
||||
domain <- reactive_get_domain(x)
|
||||
|
||||
# Convert the ... to a function that returns their evaluated values.
|
||||
keyFunc <- quos_to_func(enquos0(...))
|
||||
|
||||
valueFunc <- reactive_get_value_func(x)
|
||||
# Hash cache hint now -- this will be added to the key later on, to reduce the
|
||||
# chance of key collisions with other cachedReactives.
|
||||
cacheHint <- rlang::hash(extractCacheHint(x))
|
||||
valueFunc <- wrapFunctionLabel(valueFunc, "cachedReactiveValueFunc", ..stacktraceon = TRUE)
|
||||
|
||||
x_classes <- class(x)
|
||||
x_otel_attrs <- attr(x, "observable", exact = TRUE)$.otelAttrs
|
||||
|
||||
# Don't hold on to the reference for x, so that it can be GC'd
|
||||
rm(x)
|
||||
# Hacky workaround for issue with `%>%` preventing GC:
|
||||
# https://github.com/tidyverse/magrittr/issues/229
|
||||
if (exists(".GenericCallEnv") && exists(".", envir = .GenericCallEnv, inherits = FALSE)) {
|
||||
rm(list = ".", envir = .GenericCallEnv, inherits = FALSE)
|
||||
}
|
||||
|
||||
with_no_otel_collect({
|
||||
res <- reactive(label = label, domain = domain, {
|
||||
cache <- resolve_cache_object(cache, domain)
|
||||
hybrid_chain(
|
||||
keyFunc(),
|
||||
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook = identity, cacheWriteHook = identity)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
class(res) <- c("reactive.cache", class(res))
|
||||
|
||||
local({
|
||||
impl <- attr(res, "observable", exact = TRUE)
|
||||
impl$.otelAttrs <- append_otel_srcref_attrs(x_otel_attrs, call_srcref, fn_name = "bindCache")
|
||||
})
|
||||
|
||||
if (has_otel_collect("reactivity")) {
|
||||
res <- enable_otel_reactive_expr(res)
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.shiny.render.function <- function(x, ..., cache = "app") {
|
||||
check_dots_unnamed()
|
||||
|
||||
keyFunc <- quos_to_func(enquos0(...))
|
||||
|
||||
cacheHint <- rlang::hash(extractCacheHint(x))
|
||||
|
||||
cacheWriteHook <- attr(x, "cacheWriteHook", exact = TRUE) %||% identity
|
||||
cacheReadHook <- attr(x, "cacheReadHook", exact = TRUE) %||% identity
|
||||
|
||||
valueFunc <- x
|
||||
|
||||
renderFunc <- function(...) {
|
||||
domain <- getDefaultReactiveDomain()
|
||||
cache <- resolve_cache_object(cache, domain)
|
||||
|
||||
hybrid_chain(
|
||||
keyFunc(),
|
||||
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook, cacheWriteHook, ...)
|
||||
)
|
||||
}
|
||||
|
||||
# Passes over the otelAttrs from valueFunc to renderFunc
|
||||
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
|
||||
class(renderFunc) <- c("shiny.render.function.cache", class(valueFunc))
|
||||
renderFunc
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.shiny.renderPlot <- function(x, ...,
|
||||
cache = "app",
|
||||
sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2))
|
||||
{
|
||||
check_dots_unnamed()
|
||||
|
||||
valueFunc <- x
|
||||
|
||||
# Given the actual width/height of the image element in the browser, the
|
||||
# resize observer computes the width/height using sizePolicy() and pushes
|
||||
# those values into `fitWidth` and `fitHeight`. It's done this way so that the
|
||||
# `fitWidth` and `fitHeight` only change (and cause invalidations of the key
|
||||
# expression) when the rendered image size changes, and not every time the
|
||||
# browser's <img> tag changes size.
|
||||
#
|
||||
# If the key expression were invalidated every time the image element changed
|
||||
# size, even if the resulting key was the same (because `sizePolicy()` gave
|
||||
# the same output for a slightly different img element size), it would result
|
||||
# in getting the (same) image from the cache and sending it to the client
|
||||
# again. This resize observer prevents that.
|
||||
fitDims <- reactiveVal(NULL)
|
||||
resizeObserverCreated <- FALSE
|
||||
outputName <- NULL
|
||||
ensureResizeObserver <- function() {
|
||||
if (resizeObserverCreated)
|
||||
return()
|
||||
|
||||
doResizeCheck <- function() {
|
||||
if (is.null(outputName)) {
|
||||
outputName <<- getCurrentOutputInfo()$name
|
||||
}
|
||||
session <- getDefaultReactiveDomain()
|
||||
|
||||
width <- session$clientData[[paste0('output_', outputName, '_width')]] %||% 0
|
||||
height <- session$clientData[[paste0('output_', outputName, '_height')]] %||% 0
|
||||
|
||||
rect <- sizePolicy(c(width, height))
|
||||
fitDims(list(width = rect[1], height = rect[2]))
|
||||
}
|
||||
|
||||
# Run it once immediately, then set up the observer
|
||||
isolate(doResizeCheck())
|
||||
|
||||
observe({
|
||||
doResizeCheck()
|
||||
}, label = "plot-resize")
|
||||
# TODO: Make sure this observer gets GC'd if output$foo is replaced.
|
||||
# Currently, if you reassign output$foo, the observer persists until the
|
||||
# session ends. This is generally bad programming practice and should be
|
||||
# rare, but still, we should try to clean up properly.
|
||||
|
||||
resizeObserverCreated <<- TRUE
|
||||
}
|
||||
|
||||
renderFunc <- function(...) {
|
||||
hybrid_chain(
|
||||
# Pass in fitDims so that so that the generated plot will be the
|
||||
# dimensions specified by the sizePolicy; otherwise the plot would be the
|
||||
# exact dimensions of the img element, which isn't what we want for cached
|
||||
# plots.
|
||||
valueFunc(..., get_dims = fitDims),
|
||||
function(img) {
|
||||
# Replace exact pixel dimensions; instead, the max-height and max-width
|
||||
# will be set to 100% from CSS.
|
||||
img$class <- "shiny-scalable"
|
||||
img$width <- NULL
|
||||
img$height <- NULL
|
||||
|
||||
img
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
|
||||
class(renderFunc) <- class(valueFunc)
|
||||
|
||||
bindCache.shiny.render.function(
|
||||
renderFunc,
|
||||
...,
|
||||
{
|
||||
ensureResizeObserver()
|
||||
session <- getDefaultReactiveDomain()
|
||||
if (is.null(session) || is.null(fitDims())) {
|
||||
req(FALSE)
|
||||
}
|
||||
pixelratio <- session$clientData$pixelratio %||% 1
|
||||
|
||||
list(fitDims(), pixelratio)
|
||||
},
|
||||
cache = cache
|
||||
)
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.reactive.cache <- function(x, ...) {
|
||||
stop("bindCache() has already been called on the object.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.shiny.render.function.cache <- bindCache.reactive.cache
|
||||
|
||||
#' @export
|
||||
bindCache.reactive.event <- function(x, ...) {
|
||||
stop("Can't call bindCache() after calling bindEvent() on an object. Maybe you wanted to call bindEvent() after bindCache()?")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.shiny.render.function.event <- bindCache.reactive.event
|
||||
|
||||
#' @export
|
||||
bindCache.Observer <- function(x, ...) {
|
||||
stop("Can't bindCache an observer, because observers exist for the side efects, not for their return values.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.function <- function(x, ...) {
|
||||
stop(
|
||||
"Don't know how to add caching to a plain function. ",
|
||||
"If this is a render* function for Shiny, it may need to be updated. ",
|
||||
"Please see ?shiny::bindCache for more information."
|
||||
)
|
||||
}
|
||||
|
||||
# Returns a function which should be passed as a step in to hybrid_chain(). The
|
||||
# returned function takes a cache key as input and manages storing and retrieving
|
||||
# values from the cache, as well as executing the valueFunc if needed.
|
||||
generateCacheFun <- function(
|
||||
valueFunc,
|
||||
cache,
|
||||
cacheHint,
|
||||
cacheReadHook,
|
||||
cacheWriteHook,
|
||||
...
|
||||
) {
|
||||
function(cacheKeyResult) {
|
||||
key_str <- rlang::hash(list(cacheKeyResult, cacheHint))
|
||||
res <- cache$get(key_str)
|
||||
|
||||
# Case 1: cache hit
|
||||
if (!is.key_missing(res)) {
|
||||
return(hybrid_chain(
|
||||
{
|
||||
# The first step is just to convert `res` to a promise or not, so
|
||||
# that hybrid_chain() knows to propagate the promise-ness.
|
||||
if (res$is_promise) promise_resolve(res)
|
||||
else res
|
||||
},
|
||||
function(res) {
|
||||
if (res$error) {
|
||||
stop(res$value)
|
||||
}
|
||||
|
||||
cacheReadHook(valueWithVisible(res))
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
# Case 2: cache miss
|
||||
#
|
||||
# valueFunc() might return a promise, or an actual value. Normally we'd
|
||||
# use a hybrid_chain() for this, but in this case, we need to have
|
||||
# different behavior if it's a promise or not a promise -- the
|
||||
# information about whether or not it's a promise needs to be stored in
|
||||
# the cache. We need to handle both cases and record in the cache
|
||||
# whether it's a promise or not, so that any consumer of the
|
||||
# cachedReactive() will be given the correct kind of object (a promise
|
||||
# vs. an actual value) in the case of a future cache hit.
|
||||
p <- withCallingHandlers(
|
||||
withVisible(isolate(valueFunc(...))),
|
||||
error = function(e) {
|
||||
cache$set(key_str, list(
|
||||
is_promise = FALSE,
|
||||
value = e,
|
||||
visible = TRUE,
|
||||
error = TRUE
|
||||
))
|
||||
}
|
||||
)
|
||||
|
||||
if (is.promising(p$value)) {
|
||||
p$value <- as.promise(p$value)
|
||||
p$value <- p$value$
|
||||
then(function(value) {
|
||||
res <- withVisible(value)
|
||||
cache$set(key_str, list(
|
||||
is_promise = TRUE,
|
||||
value = cacheWriteHook(res$value),
|
||||
visible = res$visible,
|
||||
error = FALSE
|
||||
))
|
||||
valueWithVisible(res)
|
||||
})$
|
||||
catch(function(e) {
|
||||
cache$set(key_str, list(
|
||||
is_promise = TRUE,
|
||||
value = e,
|
||||
visible = TRUE,
|
||||
error = TRUE
|
||||
))
|
||||
stop(e)
|
||||
})
|
||||
valueWithVisible(p)
|
||||
} else {
|
||||
# result is an ordinary value, not a promise.
|
||||
cache$set(key_str, list(
|
||||
is_promise = FALSE,
|
||||
value = cacheWriteHook(p$value),
|
||||
visible = p$visible,
|
||||
error = FALSE
|
||||
))
|
||||
return(valueWithVisible(p))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extractCacheHint <- function(func) {
|
||||
cacheHint <- attr(func, "cacheHint", exact = TRUE)
|
||||
|
||||
if (is_false(cacheHint)) {
|
||||
stop(
|
||||
"Cannot call `bindCache()` on this object because it is marked as not cacheable.",
|
||||
call. = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
if (is.null(cacheHint)) {
|
||||
warning("No cacheHint found for this object. ",
|
||||
"Caching may not work properly.")
|
||||
}
|
||||
|
||||
cacheHint
|
||||
}
|
||||
360
R/bind-event.R
Normal file
360
R/bind-event.R
Normal file
@@ -0,0 +1,360 @@
|
||||
#' Make an object respond only to specified reactive events
|
||||
#'
|
||||
#' @description
|
||||
#'
|
||||
#' Modify an object to respond to "event-like" reactive inputs, values, and
|
||||
#' expressions. `bindEvent()` can be used with reactive expressions, render
|
||||
#' functions, and observers. The resulting object takes a reactive dependency on
|
||||
#' the `...` arguments, and not on the original object's code. This can, for
|
||||
#' example, be used to make an observer execute only when a button is pressed.
|
||||
#'
|
||||
#' `bindEvent()` was added in Shiny 1.6.0. When it is used with [reactive()] and
|
||||
#' [observe()], it does the same thing as [eventReactive()] and
|
||||
#' [observeEvent()]. However, `bindEvent()` is more flexible: it can be combined
|
||||
#' with [bindCache()], and it can also be used with `render` functions (like
|
||||
#' [renderText()] and [renderPlot()]).
|
||||
#'
|
||||
#' @section Details:
|
||||
#'
|
||||
#' Shiny's reactive programming framework is primarily designed for calculated
|
||||
#' values (reactive expressions) and side-effect-causing actions (observers)
|
||||
#' that respond to *any* of their inputs changing. That's often what is
|
||||
#' desired in Shiny apps, but not always: sometimes you want to wait for a
|
||||
#' specific action to be taken from the user, like clicking an
|
||||
#' [actionButton()], before calculating an expression or taking an action. A
|
||||
#' reactive value or expression that is used to trigger other calculations in
|
||||
#' this way is called an *event*.
|
||||
#'
|
||||
#' These situations demand a more imperative, "event handling" style of
|
||||
#' programming that is possible--but not particularly intuitive--using the
|
||||
#' reactive programming primitives [observe()] and [isolate()]. `bindEvent()`
|
||||
#' provides a straightforward API for event handling that wraps `observe` and
|
||||
#' `isolate`.
|
||||
#'
|
||||
#' The `...` arguments are captured as expressions and combined into an
|
||||
#' **event expression**. When this event expression is invalidated (when its
|
||||
#' upstream reactive inputs change), that is an **event**, and it will cause
|
||||
#' the original object's code to execute.
|
||||
#'
|
||||
#' Use `bindEvent()` with `observe()` whenever you want to *perform an action*
|
||||
#' in response to an event. (This does the same thing as [observeEvent()],
|
||||
#' which was available in Shiny prior to version 1.6.0.) Note that
|
||||
#' "recalculate a value" does not generally count as performing an action --
|
||||
#' use [reactive()] for that.
|
||||
#'
|
||||
#' Use `bindEvent()` with `reactive()` to create a *calculated value* that
|
||||
#' only updates in response to an event. This is just like a normal [reactive
|
||||
#' expression][reactive] except it ignores all the usual invalidations that
|
||||
#' come from its reactive dependencies; it only invalidates in response to the
|
||||
#' given event. (This does the same thing as [eventReactive()], which was
|
||||
#' available in Shiny prior to version 1.6.0.)
|
||||
#'
|
||||
#' `bindEvent()` is often used with [bindCache()].
|
||||
#'
|
||||
#' @section ignoreNULL and ignoreInit:
|
||||
#'
|
||||
#' `bindEvent()` takes an `ignoreNULL` parameter that affects behavior when
|
||||
#' the event expression evaluates to `NULL` (or in the special case of an
|
||||
#' [actionButton()], `0`). In these cases, if `ignoreNULL` is `TRUE`, then it
|
||||
#' will raise a silent [validation][validate] error. This is useful behavior
|
||||
#' if you don't want to do the action or calculation when your app first
|
||||
#' starts, but wait for the user to initiate the action first (like a "Submit"
|
||||
#' button); whereas `ignoreNULL=FALSE` is desirable if you want to initially
|
||||
#' perform the action/calculation and just let the user re-initiate it (like a
|
||||
#' "Recalculate" button).
|
||||
#'
|
||||
#' `bindEvent()` also takes an `ignoreInit` argument. By default, reactive
|
||||
#' expressions and observers will run on the first reactive flush after they
|
||||
#' are created (except if, at that moment, the event expression evaluates to
|
||||
#' `NULL` and `ignoreNULL` is `TRUE`). But when responding to a click of an
|
||||
#' action button, it may often be useful to set `ignoreInit` to `TRUE`. For
|
||||
#' example, if you're setting up an observer to respond to a dynamically
|
||||
#' created button, then `ignoreInit = TRUE` will guarantee that the action
|
||||
#' will only be triggered when the button is actually clicked, instead of also
|
||||
#' being triggered when it is created/initialized. Similarly, if you're
|
||||
#' setting up a reactive that responds to a dynamically created button used to
|
||||
#' refresh some data (which is then returned by that `reactive`), then you
|
||||
#' should use `reactive(...) %>% bindEvent(..., ignoreInit = TRUE)` if you
|
||||
#' want to let the user decide if/when they want to refresh the data (since,
|
||||
#' depending on the app, this may be a computationally expensive operation).
|
||||
#'
|
||||
#' Even though `ignoreNULL` and `ignoreInit` can be used for similar purposes
|
||||
#' they are independent from one another. Here's the result of combining
|
||||
#' these:
|
||||
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`ignoreNULL = TRUE` and `ignoreInit = FALSE`}{
|
||||
#' This is the default. This combination means that reactive/observer code
|
||||
#' will run every time that event expression is not
|
||||
#' `NULL`. If, at the time of creation, the event expression happens
|
||||
#' to *not* be `NULL`, then the code runs.
|
||||
#' }
|
||||
#' \item{`ignoreNULL = FALSE` and `ignoreInit = FALSE`}{
|
||||
#' This combination means that reactive/observer code will
|
||||
#' run every time no matter what.
|
||||
#' }
|
||||
#' \item{`ignoreNULL = FALSE` and `ignoreInit = TRUE`}{
|
||||
#' This combination means that reactive/observer code will
|
||||
#' *not* run at the time of creation (because `ignoreInit = TRUE`),
|
||||
#' but it will run every other time.
|
||||
#' }
|
||||
#' \item{`ignoreNULL = TRUE` and `ignoreInit = TRUE`}{
|
||||
#' This combination means that reactive/observer code will
|
||||
#' *not* at the time of creation (because `ignoreInit = TRUE`).
|
||||
#' After that, the reactive/observer code will run every time that
|
||||
#' the event expression is not `NULL`.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
|
||||
#' @section Types of objects:
|
||||
#'
|
||||
#' `bindEvent()` can be used with reactive expressions, observers, and shiny
|
||||
#' render functions.
|
||||
#'
|
||||
#' When `bindEvent()` is used with `reactive()`, it creates a new reactive
|
||||
#' expression object.
|
||||
#'
|
||||
#' When `bindEvent()` is used with `observe()`, it alters the observer in
|
||||
#' place. It can only be used with observers which have not yet executed.
|
||||
#'
|
||||
#' @section Combining events and caching:
|
||||
#'
|
||||
#' In many cases, it makes sense to use `bindEvent()` along with
|
||||
#' `bindCache()`, because they each can reduce the amount of work done on the
|
||||
#' server. For example, you could have [sliderInput]s `x` and `y` and a
|
||||
#' `reactive()` that performs a time-consuming operation with those values.
|
||||
#' Using `bindCache()` can speed things up, especially if there are multiple
|
||||
#' users. But it might make sense to also not do the computation until the
|
||||
#' user sets both `x` and `y`, and then clicks on an [actionButton] named
|
||||
#' `go`.
|
||||
#'
|
||||
#' To use both caching and events, the object should first be passed to
|
||||
#' `bindCache()`, then `bindEvent()`. For example:
|
||||
|
||||
#'
|
||||
#' ```
|
||||
#' r <- reactive({
|
||||
#' Sys.sleep(2) # Pretend this is an expensive computation
|
||||
#' input$x * input$y
|
||||
#' }) %>%
|
||||
#' bindCache(input$x, input$y) %>%
|
||||
#' bindEvent(input$go)
|
||||
#' ```
|
||||
|
||||
#'
|
||||
#' Anything that consumes `r()` will take a reactive dependency on the event
|
||||
#' expression given to `bindEvent()`, and not the cache key expression given to
|
||||
#' `bindCache()`. In this case, it is just `input$go`.
|
||||
#'
|
||||
#' @param x An object to wrap so that is triggered only when a the specified
|
||||
#' event occurs.
|
||||
#' @param ignoreNULL Whether the action should be triggered (or value
|
||||
#' calculated) when the input is `NULL`. See Details.
|
||||
#' @param ignoreInit If `TRUE`, then, when the eventified object is first
|
||||
#' created/initialized, don't trigger the action or (compute the value). The
|
||||
#' default is `FALSE`. See Details.
|
||||
#' @param once Used only for observers. Whether this `observer` should be
|
||||
#' immediately destroyed after the first time that the code in the observer is
|
||||
#' run. This pattern is useful when you want to subscribe to a event that
|
||||
#' should only happen once.
|
||||
#' @param label A label for the observer or reactive, useful for debugging.
|
||||
#' @param ... One or more expressions that represents the event; this can be a
|
||||
#' simple reactive value like `input$click`, a call to a reactive expression
|
||||
#' like `dataset()`, or even a complex expression inside curly braces. If
|
||||
#' there are multiple expressions in the `...`, then it will take a dependency
|
||||
#' on all of them.
|
||||
#' @export
|
||||
bindEvent <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
|
||||
once = FALSE, label = NULL)
|
||||
{
|
||||
check_dots_unnamed()
|
||||
force(ignoreNULL)
|
||||
force(ignoreInit)
|
||||
force(once)
|
||||
|
||||
UseMethod("bindEvent")
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.default <- function(x, ...) {
|
||||
stop("Don't know how to handle object with class ", paste(class(x), collapse = ", "))
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.reactiveExpr <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
|
||||
label = NULL)
|
||||
{
|
||||
domain <- reactive_get_domain(x)
|
||||
|
||||
qs <- enquos0(...)
|
||||
eventFunc <- quos_to_func(qs)
|
||||
|
||||
valueFunc <- reactive_get_value_func(x)
|
||||
valueFunc <- wrapFunctionLabel(valueFunc, "eventReactiveValueFunc", ..stacktraceon = TRUE)
|
||||
|
||||
call_srcref <- get_call_srcref(-1)
|
||||
if (is.null(label)) {
|
||||
label <- rassignSrcrefToLabel(
|
||||
call_srcref,
|
||||
defaultLabel = as_default_label(sprintf(
|
||||
'bindEvent(%s, %s)',
|
||||
attr(x, "observable", exact = TRUE)$.label,
|
||||
quos_to_label(qs)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
x_classes <- class(x)
|
||||
x_otel_attrs <- attr(x, "observable", exact = TRUE)$.otelAttrs
|
||||
|
||||
# Don't hold on to the reference for x, so that it can be GC'd
|
||||
rm(x)
|
||||
|
||||
initialized <- FALSE
|
||||
|
||||
with_no_otel_collect({
|
||||
res <- reactive(label = label, domain = domain, ..stacktraceon = FALSE, {
|
||||
hybrid_chain(
|
||||
{
|
||||
eventFunc()
|
||||
},
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
req(FALSE)
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(value))
|
||||
|
||||
isolate(valueFunc())
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
class(res) <- c("reactive.event", x_classes)
|
||||
|
||||
local({
|
||||
impl <- attr(res, "observable", exact = TRUE)
|
||||
impl$.otelAttrs <- append_otel_srcref_attrs(x_otel_attrs, call_srcref, fn_name = "bindEvent")
|
||||
})
|
||||
|
||||
|
||||
if (has_otel_collect("reactivity")) {
|
||||
res <- enable_otel_reactive_expr(res)
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.shiny.render.function <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE) {
|
||||
eventFunc <- quos_to_func(enquos0(...))
|
||||
|
||||
valueFunc <- x
|
||||
|
||||
initialized <- FALSE
|
||||
|
||||
renderFunc <- function(...) {
|
||||
hybrid_chain(
|
||||
eventFunc(),
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
req(FALSE)
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(value))
|
||||
|
||||
isolate(valueFunc(...))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# Passes over the otelAttrs from valueFunc to renderFunc
|
||||
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
|
||||
class(renderFunc) <- c("shiny.render.function.event", class(valueFunc))
|
||||
renderFunc
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.Observer <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
|
||||
once = FALSE, label = NULL)
|
||||
{
|
||||
if (x$.execCount > 0) {
|
||||
stop("Cannot call bindEvent() on an Observer that has already been executed.")
|
||||
}
|
||||
|
||||
qs <- enquos0(...)
|
||||
eventFunc <- quos_to_func(qs)
|
||||
valueFunc <- x$.func
|
||||
|
||||
# Note that because the observer will already have been logged by this point,
|
||||
# this updated label won't show up in the reactlog.
|
||||
if (is.null(label)) {
|
||||
call_srcref <- get_call_srcref(-1)
|
||||
x$.label <- rassignSrcrefToLabel(
|
||||
call_srcref,
|
||||
defaultLabel = as_default_label(
|
||||
sprintf('bindEvent(%s, %s)', x$.label, quos_to_label(qs))
|
||||
)
|
||||
)
|
||||
} else {
|
||||
x$.label <- label
|
||||
}
|
||||
|
||||
initialized <- FALSE
|
||||
|
||||
x$.func <- wrapFunctionLabel(
|
||||
name = x$.label,
|
||||
..stacktraceon = FALSE,
|
||||
func = function() {
|
||||
hybrid_chain(
|
||||
eventFunc(),
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
return()
|
||||
}
|
||||
|
||||
if (ignoreNULL && isNullEvent(value)) {
|
||||
return()
|
||||
}
|
||||
|
||||
if (once) {
|
||||
on.exit(x$destroy())
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(value))
|
||||
|
||||
isolate(valueFunc())
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
class(x) <- c("Observer.event", class(x))
|
||||
call_srcref <- get_call_srcref(-1)
|
||||
x$.otelAttrs <- append_otel_srcref_attrs(x$.otelAttrs, call_srcref, fn_name = "bindEvent")
|
||||
|
||||
if (has_otel_collect("reactivity")) {
|
||||
x <- enable_otel_observe(x)
|
||||
}
|
||||
|
||||
invisible(x)
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.reactive.event <- function(x, ...) {
|
||||
stop("bindEvent() has already been called on the object.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindEvent.Observer.event <- bindEvent.reactive.event
|
||||
28
R/bookmark-state-local.R
Normal file
28
R/bookmark-state-local.R
Normal file
@@ -0,0 +1,28 @@
|
||||
# Function wrappers for saving and restoring state to/from disk when running
|
||||
# Shiny locally.
|
||||
#
|
||||
# These functions provide a directory to the callback function.
|
||||
#
|
||||
# @param id A session ID to save.
|
||||
# @param callback A callback function that saves state to or restores state from
|
||||
# a directory. It must take one argument, \code{stateDir}, which is a
|
||||
# directory to which it writes/reads.
|
||||
|
||||
saveInterfaceLocal <- function(id, callback) {
|
||||
# Try to save in app directory
|
||||
appDir <- getShinyOption("appDir", default = getwd())
|
||||
|
||||
stateDir <- file.path(appDir, "shiny_bookmarks", id)
|
||||
if (!dirExists(stateDir))
|
||||
dir.create(stateDir, recursive = TRUE)
|
||||
|
||||
callback(stateDir)
|
||||
}
|
||||
|
||||
loadInterfaceLocal <- function(id, callback) {
|
||||
# Try to load from app directory
|
||||
appDir <- getShinyOption("appDir", default = getwd())
|
||||
|
||||
stateDir <- file.path(appDir, "shiny_bookmarks", id)
|
||||
callback(stateDir)
|
||||
}
|
||||
1230
R/bookmark-state.R
Normal file
1230
R/bookmark-state.R
Normal file
File diff suppressed because it is too large
Load Diff
47
R/bootstrap-deprecated.R
Normal file
47
R/bootstrap-deprecated.R
Normal file
@@ -0,0 +1,47 @@
|
||||
#' Create a page with a sidebar
|
||||
#'
|
||||
#' **DEPRECATED**: use [fluidPage()] and [sidebarLayout()] instead.
|
||||
#'
|
||||
#' @param headerPanel The [headerPanel] with the application title
|
||||
#' @param sidebarPanel The [sidebarPanel] containing input controls
|
||||
#' @param mainPanel The [mainPanel] containing outputs
|
||||
#' @keywords internal
|
||||
#' @return A UI definition that can be passed to the [shinyUI] function
|
||||
#' @export
|
||||
pageWithSidebar <- function(headerPanel,
|
||||
sidebarPanel,
|
||||
mainPanel) {
|
||||
|
||||
bootstrapPage(
|
||||
# basic application container divs
|
||||
div(
|
||||
class="container-fluid",
|
||||
div(class="row",
|
||||
headerPanel
|
||||
),
|
||||
div(class="row",
|
||||
sidebarPanel,
|
||||
mainPanel
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#' Create a header panel
|
||||
#'
|
||||
#' **DEPRECATED**: use [titlePanel()] instead.
|
||||
#'
|
||||
#' @param title An application title to display
|
||||
#' @param windowTitle The title that should be displayed by the browser window.
|
||||
#' Useful if `title` is not a string.
|
||||
#' @return A headerPanel that can be passed to [pageWithSidebar]
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
headerPanel <- function(title, windowTitle=title) {
|
||||
tagList(
|
||||
tags$head(tags$title(windowTitle)),
|
||||
div(class="col-sm-12",
|
||||
h1(title)
|
||||
)
|
||||
)
|
||||
}
|
||||
691
R/bootstrap-layout.R
Normal file
691
R/bootstrap-layout.R
Normal file
@@ -0,0 +1,691 @@
|
||||
|
||||
#' Create a page with fluid layout
|
||||
#'
|
||||
#' Functions for creating fluid page layouts. A fluid page layout consists of
|
||||
#' rows which in turn include columns. Rows exist for the purpose of making sure
|
||||
#' their elements appear on the same line (if the browser has adequate width).
|
||||
#' Columns exist for the purpose of defining how much horizontal space within a
|
||||
#' 12-unit wide grid it's elements should occupy. Fluid pages scale their
|
||||
#' components in realtime to fill all available browser width.
|
||||
#'
|
||||
#' @param ... Elements to include within the page
|
||||
#' @param title The browser window title (defaults to the host URL of the page).
|
||||
#' Can also be set as a side effect of the [titlePanel()] function.
|
||||
#' @inheritParams bootstrapPage
|
||||
#'
|
||||
#' @return A UI definition that can be passed to the [shinyUI] function.
|
||||
#'
|
||||
#' @details To create a fluid page use the `fluidPage` function and include
|
||||
#' instances of `fluidRow` and [column()] within it. As an
|
||||
#' alternative to low-level row and column functions you can also use
|
||||
#' higher-level layout functions like [sidebarLayout()].
|
||||
#'
|
||||
#' @note See the [
|
||||
#' Shiny-Application-Layout-Guide](https://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fluid
|
||||
#' pages.
|
||||
#'
|
||||
#' @family layout functions
|
||||
#' @seealso [column()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # Example of UI with fluidPage
|
||||
#' ui <- fluidPage(
|
||||
#'
|
||||
#' # Application title
|
||||
#' titlePanel("Hello Shiny!"),
|
||||
#'
|
||||
#' sidebarLayout(
|
||||
#'
|
||||
#' # Sidebar with a slider input
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("obs",
|
||||
#' "Number of observations:",
|
||||
#' min = 0,
|
||||
#' max = 1000,
|
||||
#' value = 500)
|
||||
#' ),
|
||||
#'
|
||||
#' # Show a plot of the generated distribution
|
||||
#' mainPanel(
|
||||
#' plotOutput("distPlot")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' # Server logic
|
||||
#' server <- function(input, output) {
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' hist(rnorm(input$obs))
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' # Complete app with UI and server components
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#'
|
||||
#' # UI demonstrating column layouts
|
||||
#' ui <- fluidPage(
|
||||
#' title = "Hello Shiny!",
|
||||
#' fluidRow(
|
||||
#' column(width = 4,
|
||||
#' "4"
|
||||
#' ),
|
||||
#' column(width = 3, offset = 2,
|
||||
#' "3 offset 2"
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#' @rdname fluidPage
|
||||
#' @export
|
||||
fluidPage <- function(..., title = NULL, theme = NULL, lang = NULL) {
|
||||
bootstrapPage(div(class = "container-fluid", ...),
|
||||
title = title,
|
||||
theme = theme,
|
||||
lang = lang)
|
||||
}
|
||||
|
||||
|
||||
#' @rdname fluidPage
|
||||
#' @export
|
||||
fluidRow <- function(...) {
|
||||
div(class = "row", ...)
|
||||
}
|
||||
|
||||
#' Create a page with a fixed layout
|
||||
#'
|
||||
#' Functions for creating fixed page layouts. A fixed page layout consists of
|
||||
#' rows which in turn include columns. Rows exist for the purpose of making sure
|
||||
#' their elements appear on the same line (if the browser has adequate width).
|
||||
#' Columns exist for the purpose of defining how much horizontal space within a
|
||||
#' 12-unit wide grid it's elements should occupy. Fixed pages limit their width
|
||||
#' to 940 pixels on a typical display, and 724px or 1170px on smaller and larger
|
||||
#' displays respectively.
|
||||
#'
|
||||
#' @param ... Elements to include within the container
|
||||
#' @param title The browser window title (defaults to the host URL of the page)
|
||||
#' @inheritParams bootstrapPage
|
||||
#'
|
||||
#' @return A UI definition that can be passed to the [shinyUI] function.
|
||||
#'
|
||||
#' @details To create a fixed page use the `fixedPage` function and include
|
||||
#' instances of `fixedRow` and [column()] within it. Note that
|
||||
#' unlike [fluidPage()], fixed pages cannot make use of higher-level
|
||||
#' layout functions like `sidebarLayout`, rather, all layout must be done
|
||||
#' with `fixedRow` and `column`.
|
||||
#'
|
||||
#' @note See the [
|
||||
#' Shiny Application Layout Guide](https://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fixed
|
||||
#' pages.
|
||||
#'
|
||||
#' @family layout functions
|
||||
#'
|
||||
#' @seealso [column()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fixedPage(
|
||||
#' title = "Hello, Shiny!",
|
||||
#' fixedRow(
|
||||
#' column(width = 4,
|
||||
#' "4"
|
||||
#' ),
|
||||
#' column(width = 3, offset = 2,
|
||||
#' "3 offset 2"
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#'
|
||||
#' @rdname fixedPage
|
||||
#' @export
|
||||
fixedPage <- function(..., title = NULL, theme = NULL, lang = NULL) {
|
||||
bootstrapPage(div(class = "container", ...),
|
||||
title = title,
|
||||
theme = theme,
|
||||
lang = lang)
|
||||
}
|
||||
|
||||
#' @rdname fixedPage
|
||||
#' @export
|
||||
fixedRow <- function(...) {
|
||||
div(class = "row", ...)
|
||||
}
|
||||
|
||||
|
||||
#' Create a column within a UI definition
|
||||
#'
|
||||
#' Create a column for use within a [fluidRow()] or
|
||||
#' [fixedRow()]
|
||||
#'
|
||||
#' @param width The grid width of the column (must be between 1 and 12)
|
||||
#' @param ... Elements to include within the column
|
||||
#' @param offset The number of columns to offset this column from the end of the
|
||||
#' previous column.
|
||||
#'
|
||||
#' @return A column that can be included within a
|
||||
#' [fluidRow()] or [fixedRow()].
|
||||
#'
|
||||
#'
|
||||
#' @seealso [fluidRow()], [fixedRow()].
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' fluidRow(
|
||||
#' column(4,
|
||||
#' sliderInput("obs", "Number of observations:",
|
||||
#' min = 1, max = 1000, value = 500)
|
||||
#' ),
|
||||
#' column(8,
|
||||
#' plotOutput("distPlot")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' hist(rnorm(input$obs))
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#'
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' fluidRow(
|
||||
#' column(width = 4,
|
||||
#' "4"
|
||||
#' ),
|
||||
#' column(width = 3, offset = 2,
|
||||
#' "3 offset 2"
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#' @export
|
||||
column <- function(width, ..., offset = 0) {
|
||||
|
||||
if (!is.numeric(width) || (width < 1) || (width > 12))
|
||||
stop("column width must be between 1 and 12")
|
||||
|
||||
colClass <- paste0("col-sm-", width)
|
||||
if (offset > 0) {
|
||||
# offset-md-x is for bootstrap 4 forward compat
|
||||
# (every size tier has been bumped up one level)
|
||||
# https://github.com/twbs/bootstrap/blob/74b8fe7/docs/4.3/migration/index.html#L659
|
||||
colClass <- paste0(colClass, " offset-md-", offset, " col-sm-offset-", offset)
|
||||
}
|
||||
div(class = colClass, ...)
|
||||
}
|
||||
|
||||
|
||||
#' Create a panel containing an application title.
|
||||
#'
|
||||
#' @param title An application title to display
|
||||
#' @param windowTitle The title that should be displayed by the browser window.
|
||||
#'
|
||||
#' @details Calling this function has the side effect of including a
|
||||
#' `title` tag within the head. You can also specify a page title
|
||||
#' explicitly using the `title` parameter of the top-level page function.
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' titlePanel("Hello Shiny!")
|
||||
#' )
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#' @export
|
||||
titlePanel <- function(title, windowTitle=title) {
|
||||
tagList(
|
||||
tags$head(tags$title(windowTitle)),
|
||||
h2(title)
|
||||
)
|
||||
}
|
||||
|
||||
#' Layout a sidebar and main area
|
||||
#'
|
||||
#' Create a layout (`sidebarLayout()`) with a sidebar (`sidebarPanel()`) and
|
||||
#' main area (`mainPanel()`). The sidebar is displayed with a distinct
|
||||
#' background color and typically contains input controls. The main
|
||||
#' area occupies 2/3 of the horizontal width and typically contains outputs.
|
||||
#'
|
||||
#' @param sidebarPanel The `sidebarPanel()` containing input controls.
|
||||
#' @param mainPanel The `mainPanel()` containing outputs.
|
||||
#' @param position The position of the sidebar relative to the main area ("left"
|
||||
#' or "right").
|
||||
#' @param fluid `TRUE` to use fluid layout; `FALSE` to use fixed
|
||||
#' layout.
|
||||
#' @param width The width of the sidebar and main panel. By default, the
|
||||
#' sidebar takes up 1/3 of the width, and the main panel 2/3. The total
|
||||
#' width must be 12 or less.
|
||||
#' @param ... Output elements to include in the sidebar/main panel.
|
||||
#'
|
||||
#' @family layout functions
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' # Define UI
|
||||
#' ui <- fluidPage(
|
||||
#'
|
||||
#' # Application title
|
||||
#' titlePanel("Hello Shiny!"),
|
||||
#'
|
||||
#' sidebarLayout(
|
||||
#'
|
||||
#' # Sidebar with a slider input
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("obs",
|
||||
#' "Number of observations:",
|
||||
#' min = 0,
|
||||
#' max = 1000,
|
||||
#' value = 500)
|
||||
#' ),
|
||||
#'
|
||||
#' # Show a plot of the generated distribution
|
||||
#' mainPanel(
|
||||
#' plotOutput("distPlot")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' # Server logic
|
||||
#' server <- function(input, output) {
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' hist(rnorm(input$obs))
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' # Complete app with UI and server components
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
sidebarLayout <- function(sidebarPanel,
|
||||
mainPanel,
|
||||
position = c("left", "right"),
|
||||
fluid = TRUE) {
|
||||
|
||||
# determine the order
|
||||
position <- match.arg(position)
|
||||
if (position == "left") {
|
||||
firstPanel <- sidebarPanel
|
||||
secondPanel <- mainPanel
|
||||
}
|
||||
else if (position == "right") {
|
||||
firstPanel <- mainPanel
|
||||
secondPanel <- sidebarPanel
|
||||
}
|
||||
|
||||
# return as as row
|
||||
if (fluid)
|
||||
fluidRow(firstPanel, secondPanel)
|
||||
else
|
||||
fixedRow(firstPanel, secondPanel)
|
||||
}
|
||||
|
||||
#' @export
|
||||
#' @rdname sidebarLayout
|
||||
sidebarPanel <- function(..., width = 4) {
|
||||
div(class=paste0("col-sm-", width),
|
||||
tags$form(class="well",
|
||||
# A11y semantic landmark for sidebar
|
||||
role="complementary",
|
||||
...
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#' @export
|
||||
#' @rdname sidebarLayout
|
||||
mainPanel <- function(..., width = 8) {
|
||||
div(class=paste0("col-sm-", width),
|
||||
# A11y semantic landmark for main region
|
||||
role="main",
|
||||
...
|
||||
)
|
||||
}
|
||||
|
||||
#' Lay out UI elements vertically
|
||||
#'
|
||||
#' Create a container that includes one or more rows of content (each element
|
||||
#' passed to the container will appear on it's own line in the UI)
|
||||
#'
|
||||
#' @param ... Elements to include within the container
|
||||
#' @param fluid `TRUE` to use fluid layout; `FALSE` to use fixed
|
||||
#' layout.
|
||||
#'
|
||||
#' @family layout functions
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' verticalLayout(
|
||||
#' a(href="http://example.com/link1", "Link One"),
|
||||
#' a(href="http://example.com/link2", "Link Two"),
|
||||
#' a(href="http://example.com/link3", "Link Three")
|
||||
#' )
|
||||
#' )
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#' @export
|
||||
verticalLayout <- function(..., fluid = TRUE) {
|
||||
lapply(list2(...), function(row) {
|
||||
col <- column(12, row)
|
||||
if (fluid)
|
||||
fluidRow(col)
|
||||
else
|
||||
fixedRow(col)
|
||||
})
|
||||
}
|
||||
|
||||
#' Flow layout
|
||||
#'
|
||||
#' Lays out elements in a left-to-right, top-to-bottom arrangement. The elements
|
||||
#' on a given row will be top-aligned with each other. This layout will not work
|
||||
#' well with elements that have a percentage-based width (e.g.
|
||||
#' [plotOutput()] at its default setting of `width = "100%"`).
|
||||
#'
|
||||
#' @param ... Unnamed arguments will become child elements of the layout. Named
|
||||
#' arguments will become HTML attributes on the outermost tag.
|
||||
#' @param cellArgs Any additional attributes that should be used for each cell
|
||||
#' of the layout.
|
||||
#'
|
||||
#' @family layout functions
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- flowLayout(
|
||||
#' numericInput("rows", "How many rows?", 5),
|
||||
#' selectInput("letter", "Which letter?", LETTERS),
|
||||
#' sliderInput("value", "What value?", 0, 100, 50)
|
||||
#' )
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#' @export
|
||||
flowLayout <- function(..., cellArgs = list()) {
|
||||
|
||||
children <- list2(...)
|
||||
childIdx <- !nzchar(names(children) %||% character(length(children)))
|
||||
attribs <- children[!childIdx]
|
||||
children <- children[childIdx]
|
||||
|
||||
do.call(tags$div, c(list(class = "shiny-flow-layout"),
|
||||
attribs,
|
||||
lapply(children, function(x) {
|
||||
do.call(tags$div, c(cellArgs, list(x)))
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
#' Input panel
|
||||
#'
|
||||
#' A [flowLayout()] with a grey border and light grey background,
|
||||
#' suitable for wrapping inputs.
|
||||
#'
|
||||
#' @param ... Input controls or other HTML elements.
|
||||
#' @export
|
||||
inputPanel <- function(...) {
|
||||
div(class = "shiny-input-panel",
|
||||
flowLayout(...)
|
||||
)
|
||||
}
|
||||
|
||||
#' Split layout
|
||||
#'
|
||||
#' Lays out elements horizontally, dividing the available horizontal space into
|
||||
#' equal parts (by default).
|
||||
#'
|
||||
#' @param ... Unnamed arguments will become child elements of the layout. Named
|
||||
#' arguments will become HTML attributes on the outermost tag.
|
||||
#' @param cellWidths Character or numeric vector indicating the widths of the
|
||||
#' individual cells. Recycling will be used if needed. Character values will
|
||||
#' be interpreted as CSS lengths (see [validateCssUnit()]), numeric
|
||||
#' values as pixels.
|
||||
#' @param cellArgs Any additional attributes that should be used for each cell
|
||||
#' of the layout.
|
||||
#'
|
||||
#' @family layout functions
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' # Server code used for all examples
|
||||
#' server <- function(input, output) {
|
||||
#' output$plot1 <- renderPlot(plot(cars))
|
||||
#' output$plot2 <- renderPlot(plot(pressure))
|
||||
#' output$plot3 <- renderPlot(plot(AirPassengers))
|
||||
#' }
|
||||
#'
|
||||
#' # Equal sizing
|
||||
#' ui <- splitLayout(
|
||||
#' plotOutput("plot1"),
|
||||
#' plotOutput("plot2")
|
||||
#' )
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' # Custom widths
|
||||
#' ui <- splitLayout(cellWidths = c("25%", "75%"),
|
||||
#' plotOutput("plot1"),
|
||||
#' plotOutput("plot2")
|
||||
#' )
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' # All cells at 300 pixels wide, with cell padding
|
||||
#' # and a border around everything
|
||||
#' ui <- splitLayout(
|
||||
#' style = "border: 1px solid silver;",
|
||||
#' cellWidths = 300,
|
||||
#' cellArgs = list(style = "padding: 6px"),
|
||||
#' plotOutput("plot1"),
|
||||
#' plotOutput("plot2"),
|
||||
#' plotOutput("plot3")
|
||||
#' )
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
|
||||
|
||||
children <- list2(...)
|
||||
childIdx <- !nzchar(names(children) %||% character(length(children)))
|
||||
attribs <- children[!childIdx]
|
||||
children <- children[childIdx]
|
||||
count <- length(children)
|
||||
|
||||
if (length(cellWidths) == 0 || isTRUE(is.na(cellWidths))) {
|
||||
cellWidths <- sprintf("%.3f%%", 100 / count)
|
||||
}
|
||||
cellWidths <- rep(cellWidths, length.out = count)
|
||||
cellWidths <- sapply(cellWidths, validateCssUnit)
|
||||
|
||||
do.call(tags$div, c(list(class = "shiny-split-layout"),
|
||||
attribs,
|
||||
mapply(children, cellWidths, FUN = function(x, w) {
|
||||
do.call(tags$div, c(
|
||||
list(style = sprintf("width: %s;", w)),
|
||||
cellArgs,
|
||||
list(x)
|
||||
))
|
||||
}, SIMPLIFY = FALSE)
|
||||
))
|
||||
}
|
||||
|
||||
#' Flex Box-based row/column layouts
|
||||
#'
|
||||
#' Creates row and column layouts with proportionally-sized cells, using the
|
||||
#' Flex Box layout model of CSS3. These can be nested to create arbitrary
|
||||
#' proportional-grid layouts. **Warning:** Flex Box is not well supported
|
||||
#' by Internet Explorer, so these functions should only be used where modern
|
||||
#' browsers can be assumed.
|
||||
#'
|
||||
#' @details If you try to use `fillRow` and `fillCol` inside of other
|
||||
#' Shiny containers, such as [sidebarLayout()],
|
||||
#' [navbarPage()], or even `tags$div`, you will probably find
|
||||
#' that they will not appear. This is due to `fillRow` and `fillCol`
|
||||
#' defaulting to `height="100%"`, which will only work inside of
|
||||
#' containers that have determined their own size (rather than shrinking to
|
||||
#' the size of their contents, as is usually the case in HTML).
|
||||
#'
|
||||
#' To avoid this problem, you have two options:
|
||||
#' \itemize{
|
||||
#' \item only use `fillRow`/`fillCol` inside of `fillPage`,
|
||||
#' `fillRow`, or `fillCol`
|
||||
#' \item provide an explicit `height` argument to
|
||||
#' `fillRow`/`fillCol`
|
||||
#' }
|
||||
#'
|
||||
#' @param ... UI objects to put in each row/column cell; each argument will
|
||||
#' occupy a single cell. (To put multiple items in a single cell, you can use
|
||||
#' [tagList()] or [div()] to combine them.) Named
|
||||
#' arguments will be used as attributes on the `div` element that
|
||||
#' encapsulates the row/column.
|
||||
#' @param flex Determines how space should be distributed to the cells. Can be a
|
||||
#' single value like `1` or `2` to evenly distribute the available
|
||||
#' space; or use a vector of numbers to specify the proportions. For example,
|
||||
#' `flex = c(2, 3)` would cause the space to be split 40%/60% between
|
||||
#' two cells. NA values will cause the corresponding cell to be sized
|
||||
#' according to its contents (without growing or shrinking).
|
||||
#' @param width,height The total amount of width and height to use for the
|
||||
#' entire row/column. For the default height of `"100%"` to be
|
||||
#' effective, the parent must be `fillPage`, another
|
||||
#' `fillRow`/`fillCol`, or some other HTML element whose height is
|
||||
#' not determined by the height of its contents.
|
||||
#'
|
||||
#' @examples
|
||||
#' # Only run this example in interactive R sessions.
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fillPage(fillRow(
|
||||
#' plotOutput("plotLeft", height = "100%"),
|
||||
#' fillCol(
|
||||
#' plotOutput("plotTopRight", height = "100%"),
|
||||
#' plotOutput("plotBottomRight", height = "100%")
|
||||
#' )
|
||||
#' ))
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' output$plotLeft <- renderPlot(plot(cars))
|
||||
#' output$plotTopRight <- renderPlot(plot(pressure))
|
||||
#' output$plotBottomRight <- renderPlot(plot(AirPassengers))
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' }
|
||||
#' @export
|
||||
fillRow <- function(..., flex = 1, width = "100%", height = "100%") {
|
||||
flexfill(..., direction = "row", flex = flex, width = width, height = height)
|
||||
}
|
||||
|
||||
#' @rdname fillRow
|
||||
#' @export
|
||||
fillCol <- function(..., flex = 1, width = "100%", height = "100%") {
|
||||
flexfill(..., direction = "column", flex = flex, width = width, height = height)
|
||||
}
|
||||
|
||||
flexfill <- function(..., direction, flex, width = width, height = height) {
|
||||
children <- list2(...)
|
||||
attrs <- list()
|
||||
|
||||
if (!is.null(names(children))) {
|
||||
attrs <- children[names(children) != ""]
|
||||
children <- children[names(children) == ""]
|
||||
}
|
||||
|
||||
if (length(flex) > length(children)) {
|
||||
flex <- flex[seq_along(children)]
|
||||
}
|
||||
|
||||
# The dimension along the main axis
|
||||
main <- switch(direction,
|
||||
row = "width",
|
||||
"row-reverse" = "width",
|
||||
column = "height",
|
||||
"column-reverse" = "height",
|
||||
stop("Unexpected direction")
|
||||
)
|
||||
# The dimension along the cross axis
|
||||
cross <- if (main == "width") "height" else "width"
|
||||
|
||||
divArgs <- list(
|
||||
class = sprintf("flexfill-container flexfill-container-%s", direction),
|
||||
style = css(
|
||||
display = "-webkit-flex",
|
||||
display = "-ms-flexbox",
|
||||
display = "flex",
|
||||
.webkit.flex.direction = direction,
|
||||
.ms.flex.direction = direction,
|
||||
flex.direction = direction,
|
||||
width = validateCssUnit(width),
|
||||
height = validateCssUnit(height)
|
||||
),
|
||||
mapply(children, flex, FUN = function(el, flexValue) {
|
||||
if (is.na(flexValue)) {
|
||||
# If the flex value is NA, then put the element in a simple flex item
|
||||
# that sizes itself (along the main axis) to its contents
|
||||
tags$div(
|
||||
class = "flexfill-item",
|
||||
style = css(
|
||||
position = "relative",
|
||||
"-webkit-flex" = "none",
|
||||
"-ms-flex" = "none",
|
||||
flex = "none"
|
||||
),
|
||||
style = paste0(main, ":auto;", cross, ":100%;"),
|
||||
el
|
||||
)
|
||||
} else if (is.numeric(flexValue)) {
|
||||
# If the flex value is numeric, we need *two* wrapper divs. The outer is
|
||||
# the flex item, and the inner is an absolute-fill div that is needed to
|
||||
# make percentage-based sizing for el work correctly. I don't understand
|
||||
# why this is needed but the truth is probably in this SO page:
|
||||
# http://stackoverflow.com/questions/15381172/css-flexbox-child-height-100
|
||||
tags$div(
|
||||
class = "flexfill-item",
|
||||
style = css(
|
||||
position = "relative",
|
||||
"-webkit-flex" = flexValue,
|
||||
"-ms-flex" = flexValue,
|
||||
flex = flexValue,
|
||||
width = "100%", height = "100%"
|
||||
),
|
||||
tags$div(
|
||||
class = "flexfill-item-inner",
|
||||
style = css(
|
||||
position = "absolute",
|
||||
top = 0, left = 0, right = 0, bottom = 0
|
||||
),
|
||||
el
|
||||
)
|
||||
)
|
||||
} else {
|
||||
stop("Unexpected flex argument: ", flexValue)
|
||||
}
|
||||
}, SIMPLIFY = FALSE, USE.NAMES = FALSE)
|
||||
)
|
||||
do.call(tags$div, c(attrs, divArgs))
|
||||
}
|
||||
1854
R/bootstrap.R
1854
R/bootstrap.R
File diff suppressed because it is too large
Load Diff
4
R/busy-indicators-spinners.R
Normal file
4
R/busy-indicators-spinners.R
Normal file
@@ -0,0 +1,4 @@
|
||||
# Generated by tools/updateSpinnerTypes.R: do not edit by hand
|
||||
.busySpinnerTypes <-
|
||||
c("ring", "ring2", "ring3", "bars", "bars2", "bars3", "pulse",
|
||||
"pulse2", "pulse3", "dots", "dots2", "dots3")
|
||||
294
R/busy-indicators.R
Normal file
294
R/busy-indicators.R
Normal file
@@ -0,0 +1,294 @@
|
||||
#' Enable/disable busy indication
|
||||
#'
|
||||
#' Busy indicators provide a visual cue to users when the server is busy
|
||||
#' calculating outputs or otherwise performing tasks (e.g., producing
|
||||
#' downloads). When enabled, a spinner is shown on each
|
||||
#' calculating/recalculating output, and a pulsing banner is shown at the top of
|
||||
#' the page when the app is otherwise busy. Busy indication is enabled by
|
||||
#' default for UI created with \pkg{bslib}, but must be enabled otherwise. To
|
||||
#' enable/disable, include the result of this function in anywhere in the app's
|
||||
#' UI.
|
||||
#'
|
||||
#' When both `spinners` and `pulse` are set to `TRUE`, the pulse is
|
||||
#' automatically disabled when spinner(s) are active. When both `spinners` and
|
||||
#' `pulse` are set to `FALSE`, no busy indication is shown (other than the
|
||||
#' graying out of recalculating outputs).
|
||||
#'
|
||||
#' @param ... Currently ignored.
|
||||
#' @param spinners Whether to show a spinner on each calculating/recalculating
|
||||
#' output.
|
||||
#' @param pulse Whether to show a pulsing banner at the top of the page when the
|
||||
#' app is busy.
|
||||
#' @param fade Whether to fade recalculating outputs. A value of `FALSE` is
|
||||
#' equivalent to `busyIndicatorOptions(fade_opacity=1)`.
|
||||
#'
|
||||
#' @export
|
||||
#' @seealso [busyIndicatorOptions()] for customizing the appearance of the busy
|
||||
#' indicators.
|
||||
#' @examplesIf rlang::is_interactive()
|
||||
#'
|
||||
#' library(bslib)
|
||||
#'
|
||||
#' ui <- page_fillable(
|
||||
#' useBusyIndicators(),
|
||||
#' card(
|
||||
#' card_header(
|
||||
#' "A plot",
|
||||
#' input_task_button("simulate", "Simulate"),
|
||||
#' class = "d-flex justify-content-between align-items-center"
|
||||
#' ),
|
||||
#' plotOutput("p"),
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$p <- renderPlot({
|
||||
#' input$simulate
|
||||
#' Sys.sleep(4)
|
||||
#' plot(x = rnorm(100), y = rnorm(100))
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE, fade = TRUE) {
|
||||
|
||||
rlang::check_dots_empty()
|
||||
|
||||
attrs <- list("shinyBusySpinners" = spinners, "shinyBusyPulse" = pulse)
|
||||
|
||||
js <- vapply(names(attrs), character(1), FUN = function(key) {
|
||||
if (attrs[[key]]) {
|
||||
sprintf("document.documentElement.dataset.%s = 'true';", key)
|
||||
} else {
|
||||
sprintf("delete document.documentElement.dataset.%s;", key)
|
||||
}
|
||||
})
|
||||
|
||||
# TODO: it'd be nice if htmltools had something like a page_attrs() that allowed us
|
||||
# to do this without needing to inject JS into the head.
|
||||
res <- tags$script(HTML(paste(js, collapse = "\n")))
|
||||
|
||||
if (!fade) {
|
||||
res <- tagList(res, fadeOptions(opacity = 1))
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#' Customize busy indicator options
|
||||
#'
|
||||
#' @description
|
||||
#' Shiny automatically includes busy indicators, which more specifically means:
|
||||
#' 1. Calculating/recalculating outputs have a spinner overlay.
|
||||
#' 2. Outputs fade out/in when recalculating.
|
||||
#' 3. When no outputs are calculating/recalculating, but Shiny is busy
|
||||
#' doing something else (e.g., a download, side-effect, etc), a page-level
|
||||
#' pulsing banner is shown.
|
||||
#'
|
||||
#' This function allows you to customize the appearance of these busy indicators
|
||||
#' by including the result of this function inside the app's UI. Note that,
|
||||
#' unless `spinner_selector` (or `fade_selector`) is specified, the spinner/fade
|
||||
#' customization applies to the parent element. If the customization should
|
||||
#' instead apply to the entire page, set `spinner_selector = 'html'` and
|
||||
#' `fade_selector = 'html'`.
|
||||
#'
|
||||
#' @param ... Currently ignored.
|
||||
#' @param spinner_type The type of spinner. Pre-bundled types include:
|
||||
#' '`r paste0(.busySpinnerTypes, collapse = "', '")`'.
|
||||
#'
|
||||
#' A path to a local SVG file can also be provided. The SVG should adhere to
|
||||
#' the following rules:
|
||||
#' * The SVG itself should contain the animation.
|
||||
#' * It should avoid absolute sizes (the spinner's containing DOM element
|
||||
#' size is set in CSS by `spinner_size`, so it should fill that container).
|
||||
#' * It should avoid setting absolute colors (the spinner's containing DOM element
|
||||
#' color is set in CSS by `spinner_color`, so it should inherit that color).
|
||||
#' @param spinner_color The color of the spinner. This can be any valid CSS
|
||||
#' color. Defaults to the app's "primary" color if Bootstrap is on the page.
|
||||
#' @param spinner_size The size of the spinner. This can be any valid CSS size.
|
||||
#' @param spinner_delay The amount of time to wait before showing the spinner.
|
||||
#' This can be any valid CSS time and can be useful for not showing the spinner
|
||||
#' if the computation finishes quickly.
|
||||
#' @param spinner_selector A character string containing a CSS selector for
|
||||
#' scoping the spinner customization. The default (`NULL`) will apply the
|
||||
#' spinner customization to the parent element of the spinner.
|
||||
#' @param fade_opacity The opacity (a number between 0 and 1) for recalculating
|
||||
#' output. Set to 1 to "disable" the fade.
|
||||
#' @param fade_selector A character string containing a CSS selector for
|
||||
#' scoping the spinner customization. The default (`NULL`) will apply the
|
||||
#' spinner customization to the parent element of the spinner.
|
||||
#' @param pulse_background A CSS background definition for the pulse. The
|
||||
#' default uses a
|
||||
#' [linear-gradient](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient)
|
||||
#' of the theme's indigo, purple, and pink colors.
|
||||
#' @param pulse_height The height of the pulsing banner. This can be any valid
|
||||
#' CSS size.
|
||||
#' @param pulse_speed The speed of the pulsing banner. This can be any valid CSS
|
||||
#' time.
|
||||
#'
|
||||
#' @export
|
||||
#' @seealso [useBusyIndicators()] to disable/enable busy indicators.
|
||||
#' @examplesIf rlang::is_interactive()
|
||||
#'
|
||||
#' library(bslib)
|
||||
#'
|
||||
#' card_ui <- function(id, spinner_type = id) {
|
||||
#' card(
|
||||
#' busyIndicatorOptions(spinner_type = spinner_type),
|
||||
#' card_header(paste("Spinner:", spinner_type)),
|
||||
#' plotOutput(shiny::NS(id, "plot"))
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' card_server <- function(id, simulate = reactive()) {
|
||||
#' moduleServer(
|
||||
#' id = id,
|
||||
#' function(input, output, session) {
|
||||
#' output$plot <- renderPlot({
|
||||
#' Sys.sleep(1)
|
||||
#' simulate()
|
||||
#' plot(x = rnorm(100), y = rnorm(100))
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' ui <- page_fillable(
|
||||
#' useBusyIndicators(),
|
||||
#' input_task_button("simulate", "Simulate", icon = icon("refresh")),
|
||||
#' layout_columns(
|
||||
#' card_ui("ring"),
|
||||
#' card_ui("bars"),
|
||||
#' card_ui("dots"),
|
||||
#' card_ui("pulse"),
|
||||
#' col_widths = 6
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' simulate <- reactive(input$simulate)
|
||||
#' card_server("ring", simulate)
|
||||
#' card_server("bars", simulate)
|
||||
#' card_server("dots", simulate)
|
||||
#' card_server("pulse", simulate)
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
busyIndicatorOptions <- function(
|
||||
...,
|
||||
spinner_type = NULL,
|
||||
spinner_color = NULL,
|
||||
spinner_size = NULL,
|
||||
spinner_delay = NULL,
|
||||
spinner_selector = NULL,
|
||||
fade_opacity = NULL,
|
||||
fade_selector = NULL,
|
||||
pulse_background = NULL,
|
||||
pulse_height = NULL,
|
||||
pulse_speed = NULL
|
||||
) {
|
||||
|
||||
rlang::check_dots_empty()
|
||||
|
||||
res <- tagList(
|
||||
spinnerOptions(
|
||||
type = spinner_type,
|
||||
color = spinner_color,
|
||||
size = spinner_size,
|
||||
delay = spinner_delay,
|
||||
selector = spinner_selector
|
||||
),
|
||||
fadeOptions(opacity = fade_opacity, selector = fade_selector),
|
||||
pulseOptions(
|
||||
background = pulse_background,
|
||||
height = pulse_height,
|
||||
speed = pulse_speed
|
||||
)
|
||||
)
|
||||
|
||||
bslib::as.card_item(dropNulls(res))
|
||||
}
|
||||
|
||||
|
||||
spinnerOptions <- function(type = NULL, color = NULL, size = NULL, delay = NULL, selector = NULL) {
|
||||
if (is.null(type) && is.null(color) && is.null(size) && is.null(delay) && is.null(selector)) {
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
url <- NULL
|
||||
if (!is.null(type)) {
|
||||
stopifnot(is.character(type) && length(type) == 1)
|
||||
if (file.exists(type) && grepl("\\.svg$", type)) {
|
||||
typeRaw <- readBin(type, "raw", n = file.info(type)$size)
|
||||
url <- sprintf("url('data:image/svg+xml;base64,%s')", rawToBase64(typeRaw))
|
||||
} else {
|
||||
type <- rlang::arg_match(type, .busySpinnerTypes)
|
||||
url <- sprintf("url('spinners/%s.svg')", type)
|
||||
}
|
||||
}
|
||||
|
||||
# Options controlled via CSS variables.
|
||||
css_vars <- htmltools::css(
|
||||
"--shiny-spinner-url" = url,
|
||||
"--shiny-spinner-color" = htmltools::parseCssColors(color),
|
||||
"--shiny-spinner-size" = htmltools::validateCssUnit(size),
|
||||
"--shiny-spinner-delay" = delay
|
||||
)
|
||||
|
||||
id <- NULL
|
||||
if (is.null(selector)) {
|
||||
id <- paste0("spinner-options-", p_randomInt(100, 1000000))
|
||||
selector <- sprintf(":has(> #%s)", id)
|
||||
}
|
||||
|
||||
css <- HTML(paste0(selector, " {", css_vars, "}"))
|
||||
|
||||
tags$style(css, id = id)
|
||||
}
|
||||
|
||||
fadeOptions <- function(opacity = NULL, selector = NULL) {
|
||||
if (is.null(opacity) && is.null(selector)) {
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
css_vars <- htmltools::css(
|
||||
"--shiny-fade-opacity" = opacity
|
||||
)
|
||||
|
||||
id <- NULL
|
||||
if (is.null(selector)) {
|
||||
id <- paste0("fade-options-", p_randomInt(100, 1000000))
|
||||
selector <- sprintf(":has(> #%s)", id)
|
||||
}
|
||||
|
||||
css <- HTML(paste0(selector, " {", css_vars, "}"))
|
||||
|
||||
tags$style(css, id = id)
|
||||
}
|
||||
|
||||
pulseOptions <- function(background = NULL, height = NULL, speed = NULL) {
|
||||
if (is.null(background) && is.null(height) && is.null(speed)) {
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
css_vars <- htmltools::css(
|
||||
"--shiny-pulse-background" = background,
|
||||
"--shiny-pulse-height" = htmltools::validateCssUnit(height),
|
||||
"--shiny-pulse-speed" = speed
|
||||
)
|
||||
|
||||
tags$style(HTML(paste0(":root {", css_vars, "}")))
|
||||
}
|
||||
|
||||
busyIndicatorDependency <- function() {
|
||||
htmlDependency(
|
||||
name = "shiny-busy-indicators",
|
||||
version = get_package_version("shiny"),
|
||||
src = "www/shared/busy-indicators",
|
||||
package = "shiny",
|
||||
stylesheet = "busy-indicators.css",
|
||||
# TODO-future: In next release make spinners and pulse opt-out
|
||||
# head = as.character(useBusyIndicators())
|
||||
)
|
||||
}
|
||||
25
R/cache-utils.R
Normal file
25
R/cache-utils.R
Normal file
@@ -0,0 +1,25 @@
|
||||
# For our purposes, cache objects must support these methods.
|
||||
is_cache_object <- function(x) {
|
||||
# Use tryCatch in case the object does not support `$`.
|
||||
tryCatch(
|
||||
is.function(x$get) && is.function(x$set),
|
||||
error = function(e) FALSE
|
||||
)
|
||||
}
|
||||
|
||||
# Given a cache object, or string "app" or "session", return appropriate cache
|
||||
# object.
|
||||
resolve_cache_object <- function(cache, session) {
|
||||
if (identical(cache, "app")) {
|
||||
cache <- getShinyOption("cache", default = NULL)
|
||||
|
||||
} else if (identical(cache, "session")) {
|
||||
cache <- session$cache
|
||||
}
|
||||
|
||||
if (is_cache_object(cache)) {
|
||||
return(cache)
|
||||
}
|
||||
|
||||
stop('`cache` must either be "app", "session", or a cache object with methods, `$get`, and `$set`.')
|
||||
}
|
||||
637
R/conditions.R
Normal file
637
R/conditions.R
Normal file
@@ -0,0 +1,637 @@
|
||||
#' Stack trace manipulation functions
|
||||
#'
|
||||
#' Advanced (borderline internal) functions for capturing, printing, and
|
||||
#' manipulating stack traces.
|
||||
#'
|
||||
#' @return `printError` and `printStackTrace` return
|
||||
#' `invisible()`. The other functions pass through the results of
|
||||
#' `expr`.
|
||||
#'
|
||||
#' @examples
|
||||
#' # Keeps tryCatch and withVisible related calls off the
|
||||
#' # pretty-printed stack trace
|
||||
#'
|
||||
#' visibleFunction1 <- function() {
|
||||
#' stop("Kaboom!")
|
||||
#' }
|
||||
#'
|
||||
#' visibleFunction2 <- function() {
|
||||
#' visibleFunction1()
|
||||
#' }
|
||||
#'
|
||||
#' hiddenFunction <- function(expr) {
|
||||
#' expr
|
||||
#' }
|
||||
#'
|
||||
#' # An example without ..stacktraceon/off.. manipulation.
|
||||
#' # The outer "try" is just to prevent example() from stopping.
|
||||
#' try({
|
||||
#' # The withLogErrors call ensures that stack traces are captured
|
||||
#' # and that errors that bubble up are logged using warning().
|
||||
#' withLogErrors({
|
||||
#' # tryCatch and withVisible are just here to add some noise to
|
||||
#' # the stack trace.
|
||||
#' tryCatch(
|
||||
#' withVisible({
|
||||
#' hiddenFunction(visibleFunction2())
|
||||
#' })
|
||||
#' )
|
||||
#' })
|
||||
#' })
|
||||
#'
|
||||
#' # Now the same example, but with ..stacktraceon/off.. to hide some
|
||||
#' # of the less-interesting bits (tryCatch and withVisible).
|
||||
#' ..stacktraceoff..({
|
||||
#' try({
|
||||
#' withLogErrors({
|
||||
#' tryCatch(
|
||||
#' withVisible(
|
||||
#' hiddenFunction(
|
||||
#' ..stacktraceon..(visibleFunction2())
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' })
|
||||
#' })
|
||||
#' })
|
||||
#'
|
||||
#'
|
||||
#' @name stacktrace
|
||||
#' @rdname stacktrace
|
||||
#' @keywords internal
|
||||
NULL
|
||||
|
||||
getCallNames <- function(calls) {
|
||||
sapply(calls, function(call) {
|
||||
if (is.function(call[[1]])) {
|
||||
"<Anonymous>"
|
||||
} else if (inherits(call[[1]], "call")) {
|
||||
paste0(format(call[[1]]), collapse = " ")
|
||||
} else if (typeof(call[[1]]) == "promise") {
|
||||
"<Promise>"
|
||||
} else {
|
||||
paste0(as.character(call[[1]]), collapse = " ")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
# A stripped down version of getCallNames() that intentionally avoids deparsing expressions.
|
||||
# Instead, it leaves expressions to be directly `rlang::hash()` (for de-duplication), which
|
||||
# is much faster than deparsing then hashing.
|
||||
getCallNamesForHash <- function(calls) {
|
||||
lapply(calls, function(call) {
|
||||
name <- call[[1L]]
|
||||
if (is.function(name)) return("<Anonymous>")
|
||||
if (typeof(name) == "promise") return("<Promise>")
|
||||
name
|
||||
})
|
||||
}
|
||||
|
||||
getLocs <- function(calls) {
|
||||
vapply(calls, function(call) {
|
||||
srcref <- attr(call, "srcref", exact = TRUE)
|
||||
if (!is.null(srcref)) {
|
||||
srcfile <- attr(srcref, "srcfile", exact = TRUE)
|
||||
if (!is.null(srcfile) && !is.null(srcfile$filename)) {
|
||||
loc <- paste0(srcfile$filename, "#", srcref[[1]])
|
||||
return(paste0(" [", loc, "]"))
|
||||
}
|
||||
}
|
||||
return("")
|
||||
}, character(1))
|
||||
}
|
||||
|
||||
getCallCategories <- function(calls) {
|
||||
vapply(calls, function(call) {
|
||||
srcref <- attr(call, "srcref", exact = TRUE)
|
||||
if (!is.null(srcref)) {
|
||||
srcfile <- attr(srcref, "srcfile", exact = TRUE)
|
||||
if (!is.null(srcfile)) {
|
||||
if (!is.null(srcfile$original)) {
|
||||
return("pkg")
|
||||
} else {
|
||||
return("user")
|
||||
}
|
||||
}
|
||||
}
|
||||
return("")
|
||||
}, character(1))
|
||||
}
|
||||
|
||||
#' @details `captureStackTraces` runs the given `expr` and if any
|
||||
#' *uncaught* errors occur, annotates them with stack trace info for use
|
||||
#' by `printError` and `printStackTrace`. It is not necessary to use
|
||||
#' `captureStackTraces` around the same expression as
|
||||
#' `withLogErrors`, as the latter includes a call to the former. Note
|
||||
#' that if `expr` contains calls (either directly or indirectly) to
|
||||
#' `try`, or `tryCatch` with an error handler, stack traces therein
|
||||
#' cannot be captured unless another `captureStackTraces` call is
|
||||
#' inserted in the interior of the `try` or `tryCatch`. This is
|
||||
#' because these calls catch the error and prevent it from traveling up to the
|
||||
#' condition handler installed by `captureStackTraces`.
|
||||
#'
|
||||
#' @param expr The expression to wrap.
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
captureStackTraces <- function(expr) {
|
||||
# Use `promises::` as it shows up in the stack trace
|
||||
promises::with_promise_domain(
|
||||
createStackTracePromiseDomain(),
|
||||
expr
|
||||
)
|
||||
}
|
||||
|
||||
#' @include globals.R
|
||||
.globals$deepStack <- NULL
|
||||
|
||||
getCallStackDigest <- function(callStack, warn = FALSE) {
|
||||
dg <- attr(callStack, "shiny.stack.digest", exact = TRUE)
|
||||
if (!is.null(dg)) {
|
||||
return(dg)
|
||||
}
|
||||
|
||||
if (isTRUE(warn)) {
|
||||
rlang::warn(
|
||||
"Call stack doesn't have a cached digest; expensively computing one now",
|
||||
.frequency = "once",
|
||||
.frequency_id = "deepstack-uncached-digest-warning"
|
||||
)
|
||||
}
|
||||
|
||||
rlang::hash(getCallNamesForHash(callStack))
|
||||
}
|
||||
|
||||
saveCallStackDigest <- function(callStack) {
|
||||
attr(callStack, "shiny.stack.digest") <- getCallStackDigest(callStack, warn = FALSE)
|
||||
callStack
|
||||
}
|
||||
|
||||
# Appends a call stack to a list of call stacks, but only if it's not already
|
||||
# in the list. The list is deduplicated by digest; ideally the digests on the
|
||||
# list are cached before calling this function (you will get a warning if not).
|
||||
appendCallStackWithDedupe <- function(lst, x) {
|
||||
digests <- vapply(lst, getCallStackDigest, character(1), warn = TRUE)
|
||||
xdigest <- getCallStackDigest(x, warn = TRUE)
|
||||
stopifnot(all(nzchar(digests)))
|
||||
stopifnot(length(xdigest) == 1)
|
||||
stopifnot(nzchar(xdigest))
|
||||
if (xdigest %in% digests) {
|
||||
return(lst)
|
||||
} else {
|
||||
return(c(lst, list(x)))
|
||||
}
|
||||
}
|
||||
|
||||
createStackTracePromiseDomain <- function() {
|
||||
# These are actually stateless, we wouldn't have to create a new one each time
|
||||
# if we didn't want to. They're pretty cheap though.
|
||||
|
||||
d <- new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
force(onFulfilled)
|
||||
# Subscription time
|
||||
if (deepStacksEnabled()) {
|
||||
currentStack <- sys.calls()
|
||||
currentParents <- sys.parents()
|
||||
attr(currentStack, "parents") <- currentParents
|
||||
currentStack <- saveCallStackDigest(currentStack)
|
||||
currentDeepStack <- .globals$deepStack
|
||||
}
|
||||
function(...) {
|
||||
# Fulfill time
|
||||
if (deepStacksEnabled()) {
|
||||
origDeepStack <- .globals$deepStack
|
||||
.globals$deepStack <- appendCallStackWithDedupe(currentDeepStack, currentStack)
|
||||
on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
|
||||
}
|
||||
|
||||
withCallingHandlers(
|
||||
onFulfilled(...),
|
||||
error = doCaptureStack
|
||||
)
|
||||
}
|
||||
},
|
||||
wrapOnRejected = function(onRejected) {
|
||||
force(onRejected)
|
||||
# Subscription time
|
||||
if (deepStacksEnabled()) {
|
||||
currentStack <- sys.calls()
|
||||
currentParents <- sys.parents()
|
||||
attr(currentStack, "parents") <- currentParents
|
||||
currentStack <- saveCallStackDigest(currentStack)
|
||||
currentDeepStack <- .globals$deepStack
|
||||
}
|
||||
function(...) {
|
||||
# Fulfill time
|
||||
if (deepStacksEnabled()) {
|
||||
origDeepStack <- .globals$deepStack
|
||||
.globals$deepStack <- appendCallStackWithDedupe(currentDeepStack, currentStack)
|
||||
on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
|
||||
}
|
||||
|
||||
withCallingHandlers(
|
||||
onRejected(...),
|
||||
error = doCaptureStack
|
||||
)
|
||||
}
|
||||
},
|
||||
wrapSync = function(expr) {
|
||||
withCallingHandlers(expr,
|
||||
error = doCaptureStack
|
||||
)
|
||||
},
|
||||
onError = doCaptureStack
|
||||
)
|
||||
}
|
||||
|
||||
deepStacksEnabled <- function() {
|
||||
getOption("shiny.deepstacktrace", TRUE)
|
||||
}
|
||||
|
||||
doCaptureStack <- function(e) {
|
||||
if (is.null(attr(e, "stack.trace", exact = TRUE))) {
|
||||
calls <- sys.calls()
|
||||
parents <- sys.parents()
|
||||
attr(calls, "parents") <- parents
|
||||
calls <- saveCallStackDigest(calls)
|
||||
attr(e, "stack.trace") <- calls
|
||||
}
|
||||
if (deepStacksEnabled()) {
|
||||
if (is.null(attr(e, "deep.stack.trace", exact = TRUE)) && !is.null(.globals$deepStack)) {
|
||||
attr(e, "deep.stack.trace") <- .globals$deepStack
|
||||
}
|
||||
}
|
||||
stop(e)
|
||||
}
|
||||
|
||||
#' @details `withLogErrors` captures stack traces and logs errors that
|
||||
#' occur in `expr`, but does allow errors to propagate beyond this point
|
||||
#' (i.e. it doesn't catch the error). The same caveats that apply to
|
||||
#' `captureStackTraces` with regard to `try`/`tryCatch` apply
|
||||
#' to `withLogErrors`.
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
withLogErrors <- function(expr,
|
||||
full = get_devmode_option("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
withCallingHandlers(
|
||||
{
|
||||
result <- captureStackTraces(expr)
|
||||
|
||||
# Handle expr being an async operation
|
||||
if (is.promise(result)) {
|
||||
result <- promises::catch(result, function(cond) {
|
||||
# Don't print shiny.silent.error (i.e. validation errors)
|
||||
if (cnd_inherits(cond, "shiny.silent.error")) {
|
||||
return()
|
||||
}
|
||||
if (isTRUE(getOption("show.error.messages"))) {
|
||||
printError(cond, full = full, offset = offset)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
result
|
||||
},
|
||||
error = function(cond) {
|
||||
# Don't print shiny.silent.error (i.e. validation errors)
|
||||
if (cnd_inherits(cond, "shiny.silent.error")) return()
|
||||
if (isTRUE(getOption("show.error.messages"))) {
|
||||
printError(cond, full = full, offset = offset)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#' @details `printError` prints the error and stack trace (if any) using
|
||||
#' `warning(immediate.=TRUE)`. `printStackTrace` prints the stack
|
||||
#' trace only.
|
||||
#'
|
||||
#' @param cond An condition object (generally, an error).
|
||||
#' @param full If `TRUE`, then every element of `sys.calls()` will be
|
||||
#' included in the stack trace. By default (`FALSE`), calls that Shiny
|
||||
#' deems uninteresting will be hidden.
|
||||
#' @param offset If `TRUE` (the default), srcrefs will be reassigned from
|
||||
#' the calls they originated from, to the destinations of those calls. If
|
||||
#' you're used to stack traces from other languages, this feels more
|
||||
#' intuitive, as the definition of the function indicated in the call and the
|
||||
#' location specified by the srcref match up. If `FALSE`, srcrefs will be
|
||||
#' left alone (traditional R treatment where the srcref is of the callsite).
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
printError <- function(cond,
|
||||
full = get_devmode_option("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
warning(call. = FALSE, immediate. = TRUE, sprintf("Error in %s: %s",
|
||||
getCallNames(list(conditionCall(cond))), conditionMessage(cond)))
|
||||
|
||||
printStackTrace(cond, full = full, offset = offset)
|
||||
}
|
||||
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
printStackTrace <- function(cond,
|
||||
full = get_devmode_option("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
stackTraces <- c(
|
||||
attr(cond, "deep.stack.trace", exact = TRUE),
|
||||
list(attr(cond, "stack.trace", exact = TRUE))
|
||||
)
|
||||
|
||||
# Stripping of stack traces is the one step where the different stack traces
|
||||
# interact. So we need to do this in one go, instead of individually within
|
||||
# printOneStackTrace.
|
||||
if (!full) {
|
||||
stripResults <- stripStackTraces(lapply(stackTraces, getCallNames))
|
||||
} else {
|
||||
# If full is TRUE, we don't want to strip anything
|
||||
stripResults <- rep_len(list(TRUE), length(stackTraces))
|
||||
}
|
||||
|
||||
mapply(
|
||||
seq_along(stackTraces),
|
||||
rev(stackTraces),
|
||||
rev(stripResults),
|
||||
FUN = function(i, trace, stripResult) {
|
||||
if (is.integer(trace)) {
|
||||
noun <- if (trace > 1L) "traces" else "trace"
|
||||
message("[ reached getOption(\"shiny.deepstacktrace\") -- omitted ", trace, " more stack ", noun, " ]")
|
||||
} else {
|
||||
if (i != 1) {
|
||||
message("From earlier call:")
|
||||
}
|
||||
printOneStackTrace(
|
||||
stackTrace = trace,
|
||||
stripResult = stripResult,
|
||||
full = full,
|
||||
offset = offset
|
||||
)
|
||||
}
|
||||
# No mapply return value--we're just printing
|
||||
NULL
|
||||
},
|
||||
SIMPLIFY = FALSE
|
||||
)
|
||||
|
||||
invisible()
|
||||
}
|
||||
|
||||
printOneStackTrace <- function(stackTrace, stripResult, full, offset) {
|
||||
calls <- offsetSrcrefs(stackTrace, offset = offset)
|
||||
callNames <- getCallNames(stackTrace)
|
||||
parents <- attr(stackTrace, "parents", exact = TRUE)
|
||||
|
||||
should_drop <- !full
|
||||
should_strip <- !full
|
||||
should_prune <- !full
|
||||
|
||||
if (should_drop) {
|
||||
toKeep <- dropTrivialFrames(callNames)
|
||||
calls <- calls[toKeep]
|
||||
callNames <- callNames[toKeep]
|
||||
parents <- parents[toKeep]
|
||||
stripResult <- stripResult[toKeep]
|
||||
}
|
||||
|
||||
toShow <- rep(TRUE, length(callNames))
|
||||
if (should_prune) {
|
||||
toShow <- toShow & pruneStackTrace(parents)
|
||||
}
|
||||
if (should_strip) {
|
||||
toShow <- toShow & stripResult
|
||||
}
|
||||
|
||||
# If we're running in testthat, hide the parts of the stack trace that can
|
||||
# vary based on how testthat was launched. It's critical that this is not
|
||||
# happen at the same time as dropTrivialFrames, which happens before
|
||||
# pruneStackTrace; because dropTrivialTestFrames removes calls from the top
|
||||
# (or bottom? whichever is the oldest?) of the stack, it breaks `parents`
|
||||
# which is based on absolute indices of calls. dropTrivialFrames gets away
|
||||
# with this because it only removes calls from the opposite side of the stack.
|
||||
toShow <- toShow & dropTrivialTestFrames(callNames)
|
||||
|
||||
st <- data.frame(
|
||||
num = rev(which(toShow)),
|
||||
call = rev(callNames[toShow]),
|
||||
loc = rev(getLocs(calls[toShow])),
|
||||
category = rev(getCallCategories(calls[toShow])),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
|
||||
if (nrow(st) == 0) {
|
||||
message(" [No stack trace available]")
|
||||
} else {
|
||||
width <- floor(log10(max(st$num))) + 1
|
||||
formatted <- paste0(
|
||||
" ",
|
||||
formatC(st$num, width = width),
|
||||
": ",
|
||||
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
|
||||
if (category == "pkg")
|
||||
cli::col_silver(name)
|
||||
else if (category == "user")
|
||||
cli::style_bold(cli::col_blue(name))
|
||||
else
|
||||
cli::col_white(name)
|
||||
}),
|
||||
"\n"
|
||||
)
|
||||
cat(file = stderr(), formatted, sep = "")
|
||||
}
|
||||
|
||||
invisible(st)
|
||||
}
|
||||
|
||||
stripStackTraces <- function(stackTraces, values = FALSE) {
|
||||
score <- 1L # >=1: show, <=0: hide
|
||||
lapply(seq_along(stackTraces), function(i) {
|
||||
res <- stripOneStackTrace(stackTraces[[i]], i != 1, score)
|
||||
score <<- res$score
|
||||
toShow <- as.logical(res$trace)
|
||||
if (values) {
|
||||
as.character(stackTraces[[i]][toShow])
|
||||
} else {
|
||||
as.logical(toShow)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stripOneStackTrace <- function(stackTrace, truncateFloor, startingScore) {
|
||||
prefix <- logical(0)
|
||||
if (truncateFloor) {
|
||||
indexOfFloor <- utils::tail(which(stackTrace == "..stacktracefloor.."), 1)
|
||||
if (length(indexOfFloor)) {
|
||||
stackTrace <- stackTrace[(indexOfFloor+1L):length(stackTrace)]
|
||||
prefix <- rep_len(FALSE, indexOfFloor)
|
||||
}
|
||||
}
|
||||
|
||||
if (length(stackTrace) == 0) {
|
||||
return(list(score = startingScore, character(0)))
|
||||
}
|
||||
|
||||
score <- rep.int(0L, length(stackTrace))
|
||||
score[stackTrace == "..stacktraceon.."] <- 1L
|
||||
score[stackTrace == "..stacktraceoff.."] <- -1L
|
||||
score <- startingScore + cumsum(score)
|
||||
|
||||
toShow <- score > 0 & !(stackTrace %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
|
||||
|
||||
|
||||
list(score = utils::tail(score, 1), trace = c(prefix, toShow))
|
||||
}
|
||||
|
||||
# Given sys.parents() (which corresponds to sys.calls()), return a logical index
|
||||
# that prunes each subtree so that only the final branch remains. The result,
|
||||
# when applied to sys.calls(), is a linear list of calls without any "wrapper"
|
||||
# functions like tryCatch, try, with, hybrid_chain, etc. While these are often
|
||||
# part of the active call stack, they rarely are helpful when trying to identify
|
||||
# a broken bit of code.
|
||||
pruneStackTrace <- function(parents) {
|
||||
# Detect nodes that are not the last child. This is necessary, but not
|
||||
# sufficient; we also need to drop nodes that are the last child, but one of
|
||||
# their ancestors is not.
|
||||
is_dupe <- duplicated(parents, fromLast = TRUE)
|
||||
|
||||
# The index of the most recently seen node that was actually kept instead of
|
||||
# dropped.
|
||||
current_node <- 0
|
||||
|
||||
# Loop over the parent indices. Anything that is not parented by current_node
|
||||
# (a.k.a. last-known-good node), or is a dupe, can be discarded. Anything that
|
||||
# is kept becomes the new current_node.
|
||||
#
|
||||
# jcheng 2022-03-18: Two more reasons a node can be kept:
|
||||
# 1. parent is 0
|
||||
# 2. parent is i
|
||||
# Not sure why either of these situations happen, but they're common when
|
||||
# interacting with rlang/dplyr errors. See issue rstudio/shiny#3250 for repro
|
||||
# cases.
|
||||
include <- vapply(seq_along(parents), function(i) {
|
||||
if ((!is_dupe[[i]] && parents[[i]] == current_node) ||
|
||||
parents[[i]] == 0 ||
|
||||
parents[[i]] == i) {
|
||||
current_node <<- i
|
||||
TRUE
|
||||
} else {
|
||||
FALSE
|
||||
}
|
||||
}, FUN.VALUE = logical(1))
|
||||
|
||||
include
|
||||
}
|
||||
|
||||
dropTrivialFrames <- function(callnames) {
|
||||
# Remove stop(), .handleSimpleError(), and h() calls from the end of
|
||||
# the calls--they don't add any helpful information. But only remove
|
||||
# the last *contiguous* block of them, and then, only if they are the
|
||||
# last thing in the calls list.
|
||||
hideable <- callnames %in% c(".handleSimpleError", "h", "base$wrapOnFulfilled")
|
||||
# What's the last that *didn't* match stop/.handleSimpleError/h?
|
||||
lastGoodCall <- max(which(!hideable))
|
||||
toRemove <- length(callnames) - lastGoodCall
|
||||
|
||||
c(
|
||||
rep_len(TRUE, length(callnames) - toRemove),
|
||||
rep_len(FALSE, toRemove)
|
||||
)
|
||||
}
|
||||
|
||||
dropTrivialTestFrames <- function(callnames) {
|
||||
if (!identical(Sys.getenv("TESTTHAT_IS_SNAPSHOT"), "true")) {
|
||||
return(rep_len(TRUE, length(callnames)))
|
||||
}
|
||||
|
||||
hideable <- callnames %in% c(
|
||||
"test",
|
||||
"devtools::test",
|
||||
"test_check",
|
||||
"testthat::test_check",
|
||||
"test_dir",
|
||||
"testthat::test_dir",
|
||||
"test_file",
|
||||
"testthat::test_file",
|
||||
"test_local",
|
||||
"testthat::test_local"
|
||||
)
|
||||
|
||||
# Remove everything from inception to calling the test
|
||||
# It shouldn't matter how you get there, just that you're finally testing
|
||||
toRemove <- max(which(hideable))
|
||||
|
||||
c(
|
||||
rep_len(FALSE, toRemove),
|
||||
rep_len(TRUE, length(callnames) - toRemove)
|
||||
)
|
||||
}
|
||||
|
||||
offsetSrcrefs <- function(calls, offset = TRUE) {
|
||||
if (offset) {
|
||||
srcrefs <- getSrcRefs(calls)
|
||||
|
||||
# Offset calls vs. srcrefs by 1 to make them more intuitive.
|
||||
# E.g. for "foo [bar.R:10]", line 10 of bar.R will be part of
|
||||
# the definition of foo().
|
||||
srcrefs <- c(utils::tail(srcrefs, -1), list(NULL))
|
||||
calls <- setSrcRefs(calls, srcrefs)
|
||||
}
|
||||
|
||||
calls
|
||||
}
|
||||
|
||||
getSrcRefs <- function(calls) {
|
||||
lapply(calls, function(call) {
|
||||
attr(call, "srcref", exact = TRUE)
|
||||
})
|
||||
}
|
||||
|
||||
setSrcRefs <- function(calls, srcrefs) {
|
||||
mapply(function(call, srcref) {
|
||||
structure(call, srcref = srcref)
|
||||
}, calls, srcrefs)
|
||||
}
|
||||
|
||||
stripStackTrace <- function(cond) {
|
||||
conditionStackTrace(cond) <- NULL
|
||||
}
|
||||
|
||||
#' @details `conditionStackTrace` and `conditionStackTrace<-` are
|
||||
#' accessor functions for getting/setting stack traces on conditions.
|
||||
#'
|
||||
#' @param cond A condition that may have previously been annotated by
|
||||
#' `captureStackTraces` (or `withLogErrors`).
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
conditionStackTrace <- function(cond) {
|
||||
attr(cond, "stack.trace", exact = TRUE)
|
||||
}
|
||||
|
||||
#' @param value The stack trace value to assign to the condition.
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
`conditionStackTrace<-` <- function(cond, value) {
|
||||
attr(cond, "stack.trace") <- value
|
||||
invisible(cond)
|
||||
}
|
||||
|
||||
#' @details The two functions `..stacktraceon..` and
|
||||
#' `..stacktraceoff..` have no runtime behavior during normal execution;
|
||||
#' they exist only to create artifacts on the stack trace (sys.call()) that
|
||||
#' instruct the stack trace pretty printer what parts of the stack trace are
|
||||
#' interesting or not. The initial state is 1 and we walk from the outermost
|
||||
#' call inwards. Each ..stacktraceoff.. decrements the state by one, and each
|
||||
#' ..stacktraceon.. increments the state by one. Any stack trace frame whose
|
||||
#' value is less than 1 is hidden, and finally, the ..stacktraceon.. and
|
||||
#' ..stacktraceoff.. calls themselves are hidden too.
|
||||
#'
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
..stacktraceon.. <- function(expr) expr
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
..stacktraceoff.. <- function(expr) expr
|
||||
|
||||
..stacktracefloor.. <- function(expr) expr
|
||||
121
R/deprecated.R
Normal file
121
R/deprecated.R
Normal file
@@ -0,0 +1,121 @@
|
||||
|
||||
#' Print message for deprecated functions in Shiny
|
||||
#'
|
||||
#' To disable these messages, use `options(shiny.deprecation.messages=FALSE)`.
|
||||
#'
|
||||
#' @param version Shiny version when the function was deprecated
|
||||
#' @param what Function with possible arguments
|
||||
#' @param with Possible function with arguments that should be used instead
|
||||
#' @param details Additional information to be added after a new line to the displayed message
|
||||
#' @keywords internal
|
||||
shinyDeprecated <- function(
|
||||
version,
|
||||
what,
|
||||
with = NULL,
|
||||
details = NULL,
|
||||
type = c("deprecated", "superseded")
|
||||
) {
|
||||
if (is_false(getOption("shiny.deprecation.messages"))) {
|
||||
return(invisible())
|
||||
}
|
||||
|
||||
type <- match.arg(type)
|
||||
|
||||
msg <- paste0("`", what, "` is ", type, " as of shiny ", version, ".")
|
||||
if (!is.null(with)) {
|
||||
msg <- paste0(msg, "\n", "Please use `", with, "` instead.")
|
||||
}
|
||||
if (!is.null(details)) {
|
||||
msg <- paste0(msg, "\n", details)
|
||||
}
|
||||
|
||||
# lifecycle::deprecate_soft(when, what, with = with, details = details, id = id, env = env)
|
||||
rlang::inform(message = msg, .frequency = "always", .frequency_id = msg, .file = stderr())
|
||||
}
|
||||
|
||||
|
||||
deprecatedEnvQuotedMessage <- function() {
|
||||
if (!in_devmode()) return(invisible())
|
||||
if (is_false(getOption("shiny.deprecation.messages"))) return(invisible())
|
||||
|
||||
# Capture calling function
|
||||
grandparent_call <- sys.call(-2)
|
||||
# Turn language into user friendly string
|
||||
grandparent_txt <- paste0(utils::capture.output({grandparent_call}), collapse = "\n")
|
||||
|
||||
msg <- paste0(
|
||||
"The `env` and `quoted` arguments are deprecated as of shiny 1.7.0.",
|
||||
" Please use quosures from `rlang` instead.\n",
|
||||
"See <https://github.com/rstudio/shiny/issues/3108> for more information.\n",
|
||||
"Function call:\n",
|
||||
grandparent_txt
|
||||
)
|
||||
# Call less often as users do not have much control over this warning
|
||||
rlang::inform(message = msg, .frequency = "regularly", .frequency_id = msg, .file = stderr())
|
||||
}
|
||||
|
||||
|
||||
#' Create disk cache (deprecated)
|
||||
#'
|
||||
#' @param exec_missing Deprecated.
|
||||
#' @inheritParams cachem::cache_disk
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
diskCache <- function(
|
||||
dir = NULL,
|
||||
max_size = 500 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
destroy_on_finalize = FALSE,
|
||||
missing = key_missing(),
|
||||
exec_missing = deprecated(),
|
||||
logfile = NULL
|
||||
) {
|
||||
shinyDeprecated("1.6.0", "diskCache()", "cachem::cache_disk()")
|
||||
if (is_present(exec_missing)) {
|
||||
shinyDeprecated("1.6.0", "diskCache(exec_missing =)")
|
||||
}
|
||||
|
||||
cachem::cache_disk(
|
||||
dir = dir,
|
||||
max_size = max_size,
|
||||
max_age = max_age,
|
||||
max_n = max_n,
|
||||
evict = evict,
|
||||
destroy_on_finalize = destroy_on_finalize,
|
||||
missing = missing,
|
||||
logfile = logfile
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' Create memory cache (deprecated)
|
||||
#'
|
||||
#' @param exec_missing Deprecated.
|
||||
#' @inheritParams cachem::cache_mem
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
memoryCache <- function(
|
||||
max_size = 200 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
missing = key_missing(),
|
||||
exec_missing = deprecated(),
|
||||
logfile = NULL)
|
||||
{
|
||||
shinyDeprecated("1.6.0", "diskCache()", "cachem::cache_mem()")
|
||||
if (is_present(exec_missing)) {
|
||||
shinyDeprecated("1.6.0", "diskCache(exec_missing =)")
|
||||
}
|
||||
|
||||
cachem::cache_mem(
|
||||
max_size = max_size,
|
||||
max_age = max_age,
|
||||
max_n = max_n,
|
||||
evict = evict,
|
||||
missing = missing,
|
||||
logfile = logfile
|
||||
)
|
||||
}
|
||||
369
R/devmode.R
Normal file
369
R/devmode.R
Normal file
@@ -0,0 +1,369 @@
|
||||
#' Shiny Developer Mode
|
||||
#'
|
||||
#' @description `r lifecycle::badge("experimental")`
|
||||
#'
|
||||
#' Developer Mode enables a number of [options()] to make a developer's life
|
||||
#' easier, like enabling non-minified JS and printing messages about
|
||||
#' deprecated functions and options.
|
||||
#'
|
||||
#' Shiny Developer Mode can be enabled by calling `devmode(TRUE)` and disabled
|
||||
#' by calling `devmode(FALSE)`.
|
||||
#'
|
||||
#' Please see the function descriptions for more details.
|
||||
#'
|
||||
#' @describeIn devmode Function to set two options to enable/disable Shiny
|
||||
#' Developer Mode and Developer messages
|
||||
#' @param devmode Logical value which should be set to `TRUE` to enable Shiny
|
||||
#' Developer Mode
|
||||
#' @param verbose Logical value which should be set to `TRUE` display Shiny
|
||||
#' Developer messages
|
||||
#' @export
|
||||
#' @examples
|
||||
#' # Enable Shiny Developer mode
|
||||
#' devmode()
|
||||
#'
|
||||
devmode <- function(
|
||||
devmode = getOption("shiny.devmode", TRUE),
|
||||
verbose = getOption("shiny.devmode.verbose", TRUE)
|
||||
) {
|
||||
options(
|
||||
shiny.devmode = devmode,
|
||||
shiny.devmode.verbose = verbose
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' @describeIn devmode Determines if Shiny is in Developer Mode. If the
|
||||
#' `getOption("shiny.devmode")` is set to `TRUE` and not in testing inside
|
||||
#' `testthat`, then Shiny Developer Mode is enabled.
|
||||
#' @section Avoiding direct dependency on shiny:
|
||||
#'
|
||||
#' The methods explained in this help file act independently from the rest of
|
||||
#' Shiny but are included to provide blue prints for your own packages. If
|
||||
#' your package already has (or is willing to take) a dependency on Shiny, we
|
||||
#' recommend using the exported Shiny methods for consistent behavior. Note
|
||||
#' that if you use exported Shiny methods, it will cause the Shiny package to
|
||||
#' load. This may be undesirable if your code will be used in (for example) R
|
||||
#' Markdown documents that do not have a Shiny runtime (`runtime: shiny`).
|
||||
#'
|
||||
#' If your package can **not** take a dependency on Shiny, we recommending
|
||||
#' re-implementing these two functions:
|
||||
#'
|
||||
#' \enumerate{
|
||||
#' \item `in_devmode()`:
|
||||
#'
|
||||
#' This function should return `TRUE` if `getOption("shiny.devmode")` is set.
|
||||
#' In addition, we strongly recommend that it also checks to make sure
|
||||
#' `testthat` is not testing.
|
||||
#'
|
||||
#' ```r
|
||||
#' in_devmode <- function() {
|
||||
#' isTRUE(getOption("shiny.devmode", FALSE)) &&
|
||||
#' !identical(Sys.getenv("TESTTHAT"), "true")
|
||||
#' }
|
||||
#' ```
|
||||
#'
|
||||
#' \item `get_devmode_option(name, default, devmode_default, devmode_message)`:
|
||||
#'
|
||||
#' This function is similar to `getOption(name, default)`, but when the option
|
||||
#' is not set, the default value changes depending on the Dev Mode.
|
||||
#' `get_devmode_option()` should be implemented as follows:
|
||||
#'
|
||||
#' * If not in Dev Mode:
|
||||
#' * Return `getOption(name, default)`.
|
||||
#' * If in Dev Mode:
|
||||
#' * Get the global option `getOption(name)` value.
|
||||
#' * If the global option value is set:
|
||||
#' * Return the value.
|
||||
#' * If the global option value is not set:
|
||||
#' * Notify the developer that the Dev Mode default value will be used.
|
||||
#' * Return the Dev Mode default value.
|
||||
#'
|
||||
#' When notifying the developer that the default value has changed, we strongly
|
||||
#' recommend displaying a message (`devmode_message`) to `stderr()` once every 8
|
||||
#' hours using [rlang::inform()]. This will keep the author up to date as to
|
||||
#' which Dev Mode options are being altered. To allow developers a chance to
|
||||
#' disable Dev Mode messages, the message should be skipped if
|
||||
#' `getOption("shiny.devmode.verbose", TRUE)` is not `TRUE`.
|
||||
#'
|
||||
#' ```r
|
||||
#' get_devmode_option <- function(name, default = NULL, devmode_default, devmode_message) {
|
||||
#' if (!in_devmode()) {
|
||||
#' # Dev Mode disabled, act like `getOption()`
|
||||
#' return(getOption(name, default = default))
|
||||
#' }
|
||||
#'
|
||||
#' # Dev Mode enabled, update the default value for `getOption()`
|
||||
#' getOption(name, default = {
|
||||
#' # Notify developer
|
||||
#' if (
|
||||
#' !missing(devmode_message) &&
|
||||
#' !is.null(devmode_message) &&
|
||||
#' getOption("shiny.devmode.verbose", TRUE)
|
||||
#' ) {
|
||||
#' rlang::inform(
|
||||
#' message = devmode_message,
|
||||
#' .frequency = "regularly",
|
||||
#' .frequency_id = devmode_message,
|
||||
#' .file = stderr()
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' # Return Dev Mode default value `devmode_default`
|
||||
#' devmode_default
|
||||
#' })
|
||||
#' }
|
||||
#' ```
|
||||
#' }
|
||||
#'
|
||||
#' The remaining functions in this file are used for author convenience and are
|
||||
#' not recommended for all reimplementation situations.
|
||||
#' @export
|
||||
#' @examples
|
||||
#' in_devmode() # TRUE/FALSE?
|
||||
#'
|
||||
in_devmode <- function() {
|
||||
isTRUE(getOption("shiny.devmode", FALSE)) &&
|
||||
# !testthat::is_testing()
|
||||
!identical(Sys.getenv("TESTTHAT"), "true")
|
||||
}
|
||||
|
||||
in_client_devmode <- function() {
|
||||
# Client-side devmode enables client-side only dev features without local
|
||||
# devmode. Currently, the main feature is the client-side error console.
|
||||
isTRUE(getOption("shiny.client_devmode", FALSE))
|
||||
}
|
||||
|
||||
#' @describeIn devmode Temporarily set Shiny Developer Mode and Developer
|
||||
#' message verbosity
|
||||
#' @param code Code to execute with the temporary Dev Mode options set
|
||||
#' @export
|
||||
#' @examples
|
||||
#' # Execute code in a temporary shiny dev mode
|
||||
#' with_devmode(TRUE, in_devmode()) # TRUE
|
||||
#'
|
||||
with_devmode <- function(
|
||||
devmode,
|
||||
code,
|
||||
verbose = getOption("shiny.devmode.verbose", TRUE)
|
||||
) {
|
||||
withr::with_options(
|
||||
list(
|
||||
shiny.devmode = devmode,
|
||||
shiny.devmode.verbose = verbose
|
||||
),
|
||||
code
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' @describeIn devmode If Shiny Developer Mode and verbosity are enabled,
|
||||
#' displays a message once every 8 hrs (by default)
|
||||
#' @param message Developer Mode message to be sent to [rlang::inform()]
|
||||
#' @param .frequency Frequency of the Developer Mode message used with
|
||||
#' [rlang::inform()]. Defaults to once every 8 hours.
|
||||
#' @param .frequency_id [rlang::inform()] message identifier. Defaults to
|
||||
#' `message`.
|
||||
#' @param .file Output connection for [rlang::inform()]. Defaults to [stderr()]
|
||||
#' @param ... Parameters passed to [rlang::inform()]
|
||||
devmode_inform <- function(
|
||||
message,
|
||||
.frequency = "regularly",
|
||||
.frequency_id = message,
|
||||
.file = stderr(),
|
||||
...
|
||||
) {
|
||||
|
||||
if (!(
|
||||
in_devmode() &&
|
||||
isTRUE(getOption("shiny.devmode.verbose", TRUE))
|
||||
)) {
|
||||
return()
|
||||
}
|
||||
if (is.null(message)) {
|
||||
return()
|
||||
}
|
||||
|
||||
rlang::inform(
|
||||
message = paste0("shiny devmode - ", message),
|
||||
.frequency = .frequency,
|
||||
.frequency_id = .frequency_id,
|
||||
.file = .file,
|
||||
...
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
registered_devmode_options <- NULL
|
||||
on_load({
|
||||
registered_devmode_options <- Map$new()
|
||||
})
|
||||
|
||||
#' @describeIn devmode Registers a Shiny Developer Mode option with an updated
|
||||
#' value and Developer message. This registration method allows package
|
||||
#' authors to write one message in a single location.
|
||||
#'
|
||||
#' For example, the following Shiny Developer Mode options are registered:
|
||||
#'
|
||||
#' ```r
|
||||
#' # Reload the Shiny app when a sourced R file changes
|
||||
#' register_devmode_option(
|
||||
#' "shiny.autoreload",
|
||||
#' "Turning on shiny autoreload. To disable, call `options(shiny.autoreload = FALSE)`",
|
||||
#' devmode_default = TRUE
|
||||
#' )
|
||||
#'
|
||||
#' # Use the unminified Shiny JavaScript file, `shiny.js`
|
||||
#' register_devmode_option(
|
||||
#' "shiny.minified",
|
||||
#' "Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`",
|
||||
#' devmode_default = FALSE
|
||||
#' )
|
||||
#'
|
||||
#' # Display the full stack trace when errors occur during Shiny app execution
|
||||
#' register_devmode_option(
|
||||
#' "shiny.fullstacktrace",
|
||||
#' "Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`",
|
||||
#' devmode_default = TRUE
|
||||
#' )
|
||||
#' ```
|
||||
#'
|
||||
#' Other known, non-Shiny Developer Mode options:
|
||||
#'
|
||||
#' * Sass:
|
||||
#' ```r
|
||||
#' # Display the full stack trace when errors occur during Shiny app execution
|
||||
#' register_devmode_option(
|
||||
#' "sass.cache",
|
||||
#' "Turning off sass cache. To use default caching, call `options(sass.cache = TRUE)`",
|
||||
#' devmode_default = FALSE
|
||||
#' )
|
||||
#' ```
|
||||
#'
|
||||
#' @param name Name of option to look for in `options()`
|
||||
#' @param default Default value to return if `in_devmode()` returns
|
||||
#' `TRUE` and the specified option is not set in [`options()`].
|
||||
#' @param devmode_message Message to display once every 8 hours when utilizing
|
||||
#' the `devmode_default` value. If `devmode_message` is missing, the
|
||||
#' registered `devmode_message` value be used.
|
||||
#' @param devmode_default Default value to return if `in_devmode()` returns
|
||||
#' `TRUE` and the specified option is not set in [`options()`]. For
|
||||
#' `get_devmode_option()`, if `devmode_default` is missing, the
|
||||
#' registered `devmode_default` value will be used.
|
||||
#' @export
|
||||
#' @examples
|
||||
#' # Ex: Within shiny, we register the option "shiny.minified"
|
||||
#' # to default to `FALSE` when in Dev Mode
|
||||
#' \dontrun{register_devmode_option(
|
||||
#' "shiny.minified",
|
||||
#' devmode_message = paste0(
|
||||
#' "Using full shiny javascript file. ",
|
||||
#' "To use the minified version, call `options(shiny.minified = TRUE)`"
|
||||
#' ),
|
||||
#' devmode_default = FALSE
|
||||
#' )}
|
||||
#'
|
||||
register_devmode_option <- function(
|
||||
name,
|
||||
devmode_message = NULL,
|
||||
devmode_default = NULL
|
||||
) {
|
||||
if (!is.null(devmode_message)) {
|
||||
stopifnot(length(devmode_message) == 1 && is.character(devmode_message))
|
||||
}
|
||||
registered_devmode_options$set(
|
||||
name,
|
||||
list(devmode_default = devmode_default, devmode_message = devmode_message)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' @describeIn devmode Provides a consistent way to change the expected
|
||||
#' [getOption()] behavior when Developer Mode is enabled. This method is very
|
||||
#' similar to [getOption()] where the globally set option takes precedence.
|
||||
#' See section "Avoiding direct dependency on shiny" for
|
||||
#' `get_devmode_option()` implementation details.
|
||||
#'
|
||||
#' **Package developers:** Register your Dev Mode option using
|
||||
#' `register_devmode_option()` to avoid supplying the same `devmode_default`
|
||||
#' and `devmode_message` values throughout your package. (This requires a
|
||||
#' \pkg{shiny} dependency.)
|
||||
#' @export
|
||||
#' @examples
|
||||
#' # Used within `shiny::runApp(launch.browser)`
|
||||
#' get_devmode_option("shiny.minified", TRUE) # TRUE if Dev mode is off
|
||||
#' is_minified <- with_devmode(TRUE, {
|
||||
#' get_devmode_option("shiny.minified", TRUE)
|
||||
#' })
|
||||
#' is_minified # FALSE
|
||||
#'
|
||||
get_devmode_option <- function(
|
||||
name,
|
||||
default = NULL,
|
||||
devmode_default = missing_arg(),
|
||||
devmode_message = missing_arg()
|
||||
) {
|
||||
getOption(
|
||||
name,
|
||||
local({
|
||||
if (!in_devmode()) {
|
||||
# typical case
|
||||
return(default)
|
||||
}
|
||||
|
||||
info <- registered_devmode_options$get(name)
|
||||
if (is.null(info)) {
|
||||
# Not registered,
|
||||
# Warn and return default value
|
||||
rlang::warn(
|
||||
message = paste0(
|
||||
"`get_devmode_option(name)` could not find `name = \"", name, "\"`. ",
|
||||
"Returning `default` value"
|
||||
)
|
||||
)
|
||||
return(default)
|
||||
}
|
||||
|
||||
# display message
|
||||
devmode_inform(
|
||||
maybe_missing(
|
||||
# use provided `devmode_message` value
|
||||
devmode_message,
|
||||
# If `devmode_message` is missing, display registered `devmode_message`
|
||||
default = info$devmode_message
|
||||
)
|
||||
)
|
||||
|
||||
# return value
|
||||
maybe_missing(
|
||||
# use provided `devmode_default` value
|
||||
devmode_default,
|
||||
# if `devmode_default` is missing, provide registered `devmode_default`
|
||||
default = info$devmode_default
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
on_load({
|
||||
register_devmode_option(
|
||||
"shiny.autoreload",
|
||||
"Turning on shiny autoreload. To disable, call `options(shiny.autoreload = FALSE)`",
|
||||
TRUE
|
||||
)
|
||||
|
||||
register_devmode_option(
|
||||
"shiny.minified",
|
||||
"Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`",
|
||||
FALSE
|
||||
)
|
||||
|
||||
register_devmode_option(
|
||||
"shiny.fullstacktrace",
|
||||
"Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`",
|
||||
TRUE
|
||||
)
|
||||
})
|
||||
157
R/diagnose.R
Normal file
157
R/diagnose.R
Normal file
@@ -0,0 +1,157 @@
|
||||
# Analyze an R file for possible extra or missing commas. Returns FALSE if any
|
||||
# problems detected, TRUE otherwise.
|
||||
diagnoseCode <- function(path = NULL, text = NULL) {
|
||||
if (!xor(is.null(path), is.null(text))) {
|
||||
stop("Must specify `path` or `text`, but not both.")
|
||||
}
|
||||
|
||||
if (!is.null(path)) {
|
||||
tokens <- sourcetools::tokenize_file(path)
|
||||
} else {
|
||||
tokens <- sourcetools::tokenize_string(text)
|
||||
}
|
||||
|
||||
find_scopes <- function(tokens) {
|
||||
# Strip whitespace and comments
|
||||
tokens <- tokens[!(tokens$type %in% c("whitespace", "comment")),]
|
||||
|
||||
# Replace various types of things with "value"
|
||||
tokens$type[tokens$type %in% c("string", "number", "symbol", "keyword")] <- "value"
|
||||
|
||||
# Record types for close and open brace/bracket/parens, and commas
|
||||
brace_idx <- tokens$value %in% c("(", ")", "{", "}", "[", "]", ",")
|
||||
tokens$type[brace_idx] <- tokens$value[brace_idx]
|
||||
|
||||
# Stack-related function for recording scope. Starting scope is "{"
|
||||
stack <- "{"
|
||||
push <- function(x) {
|
||||
stack <<- c(stack, x)
|
||||
}
|
||||
pop <- function() {
|
||||
if (length(stack) == 1) {
|
||||
# Stack underflow, but we need to keep going
|
||||
return(NA_character_)
|
||||
}
|
||||
res <- stack[length(stack)]
|
||||
stack <<- stack[-length(stack)]
|
||||
res
|
||||
}
|
||||
peek <- function() {
|
||||
stack[length(stack)]
|
||||
}
|
||||
|
||||
# First, establish a scope for each token. For opening and closing
|
||||
# braces/brackets/parens, the scope at that location is the *surrounding*
|
||||
# scope, not the new scope created by the brace/bracket/paren.
|
||||
for (i in seq_len(nrow(tokens))) {
|
||||
value <- tokens$value[i]
|
||||
|
||||
tokens$scope[i] <- peek()
|
||||
if (value %in% c("{", "(", "[")) {
|
||||
push(value)
|
||||
|
||||
} else if (value == "}") {
|
||||
if (!identical(pop(), "{"))
|
||||
tokens$err[i] <- "unmatched_brace"
|
||||
# For closing brace/paren/bracket, get the scope after popping
|
||||
tokens$scope[i] <- peek()
|
||||
|
||||
} else if (value == ")") {
|
||||
if (!identical(pop(), "("))
|
||||
tokens$err[i] <- "unmatched_paren"
|
||||
tokens$scope[i] <- peek()
|
||||
|
||||
} else if (value == "]") {
|
||||
if (!identical(pop(), "["))
|
||||
tokens$err[i] <- "unmatched_bracket"
|
||||
tokens$scope[i] <- peek()
|
||||
}
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
check_commas <- function(tokens) {
|
||||
# Find extra and missing commas
|
||||
tokens$err <- mapply(
|
||||
tokens$type,
|
||||
c("", tokens$type[-length(tokens$type)]),
|
||||
c(tokens$type[-1], ""),
|
||||
tokens$scope,
|
||||
tokens$err,
|
||||
SIMPLIFY = FALSE,
|
||||
FUN = function(type, prevType, nextType, scope, err) {
|
||||
# If an error was already found, just return it. This could have
|
||||
# happened in the brace/paren/bracket matching phase.
|
||||
if (!is.na(err)) {
|
||||
return(err)
|
||||
}
|
||||
if (scope == "(") {
|
||||
if (type == "," &&
|
||||
(prevType == "(" || prevType == "," || nextType == ")"))
|
||||
{
|
||||
return("extra_comma")
|
||||
}
|
||||
|
||||
if ((prevType == ")" && type == "value") ||
|
||||
(prevType == "value" && type == "value")) {
|
||||
return("missing_comma")
|
||||
}
|
||||
}
|
||||
|
||||
NA_character_
|
||||
}
|
||||
)
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
|
||||
tokens$err <- NA_character_
|
||||
tokens <- find_scopes(tokens)
|
||||
tokens <- check_commas(tokens)
|
||||
|
||||
# No errors found
|
||||
if (all(is.na(tokens$err))) {
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
# If we got here, errors were found; print messages.
|
||||
if (!is.null(path)) {
|
||||
lines <- readLines(path)
|
||||
} else {
|
||||
lines <- strsplit(text, "\n")[[1]]
|
||||
}
|
||||
|
||||
# Print out the line of code with the error, and point to the column with
|
||||
# the error.
|
||||
show_code_error <- function(msg, lines, row, col) {
|
||||
message(paste0(
|
||||
msg, "\n",
|
||||
row, ":", lines[row], "\n",
|
||||
paste0(rep.int(" ", nchar(as.character(row)) + 1), collapse = ""),
|
||||
gsub(perl = TRUE, "[^\\s]", " ", substr(lines[row], 1, col-1)), "^"
|
||||
))
|
||||
}
|
||||
|
||||
err_idx <- which(!is.na(tokens$err))
|
||||
msg <- ""
|
||||
for (i in err_idx) {
|
||||
row <- tokens$row[i]
|
||||
col <- tokens$column[i]
|
||||
err <- tokens$err[i]
|
||||
|
||||
if (err == "missing_comma") {
|
||||
show_code_error("Possible missing comma at:", lines, row, col)
|
||||
} else if (err == "extra_comma") {
|
||||
show_code_error("Possible extra comma at:", lines, row, col)
|
||||
} else if (err == "unmatched_brace") {
|
||||
show_code_error("Possible unmatched '}' at:", lines, row, col)
|
||||
} else if (err == "unmatched_paren") {
|
||||
show_code_error("Possible unmatched ')' at:", lines, row, col)
|
||||
} else if (err == "unmatched_bracket") {
|
||||
show_code_error("Possible unmatched ']' at:", lines, row, col)
|
||||
}
|
||||
}
|
||||
return(FALSE)
|
||||
}
|
||||
338
R/extended-task.R
Normal file
338
R/extended-task.R
Normal file
@@ -0,0 +1,338 @@
|
||||
#' Task or computation that proceeds in the background
|
||||
#'
|
||||
#' @description In normal Shiny reactive code, whenever an observer, calc, or
|
||||
#' output is busy computing, it blocks the current session from receiving any
|
||||
#' inputs or attempting to proceed with any other computation related to that
|
||||
#' session.
|
||||
#'
|
||||
#' The `ExtendedTask` class allows you to have an expensive operation that is
|
||||
#' started by a reactive effect, and whose (eventual) results can be accessed
|
||||
#' by a regular observer, calc, or output; but during the course of the
|
||||
#' operation, the current session is completely unblocked, allowing the user
|
||||
#' to continue using the rest of the app while the operation proceeds in the
|
||||
#' background.
|
||||
#'
|
||||
#' Note that each `ExtendedTask` object does not represent a _single
|
||||
#' invocation_ of its long-running function. Rather, it's an object that is
|
||||
#' used to invoke the function with different arguments, keeps track of
|
||||
#' whether an invocation is in progress, and provides ways to get at the
|
||||
#' current status or results of the operation. A single `ExtendedTask` object
|
||||
#' does not permit overlapping invocations: if the `invoke()` method is called
|
||||
#' before the previous `invoke()` is completed, the new invocation will not
|
||||
#' begin until the previous invocation has completed.
|
||||
#'
|
||||
#' @section `ExtendedTask` versus asynchronous reactives:
|
||||
#'
|
||||
#' Shiny has long supported [using
|
||||
#' \{promises\}](https://rstudio.github.io/promises/articles/promises_06_shiny.html)
|
||||
#' to write asynchronous observers, calcs, or outputs. You may be wondering
|
||||
#' what the differences are between those techniques and this class.
|
||||
#'
|
||||
#' Asynchronous observers, calcs, and outputs are not--and have never
|
||||
#' been--designed to let a user start a long-running operation, while keeping
|
||||
#' that very same (browser) session responsive to other interactions. Instead,
|
||||
#' they unblock other sessions, so you can take a long-running operation that
|
||||
#' would normally bring the entire R process to a halt and limit the blocking
|
||||
#' to just the session that started the operation. (For more details, see the
|
||||
#' section on ["The Flush
|
||||
#' Cycle"](https://rstudio.github.io/promises/articles/promises_06_shiny.html#the-flush-cycle).)
|
||||
#'
|
||||
#' `ExtendedTask`, on the other hand, invokes an asynchronous function (that
|
||||
#' is, a function that quickly returns a promise) and allows even that very
|
||||
#' session to immediately unblock and carry on with other user interactions.
|
||||
#'
|
||||
#' @section OpenTelemetry Integration:
|
||||
#'
|
||||
#' When an `ExtendedTask` is created, if OpenTelemetry tracing is enabled for
|
||||
#' `"reactivity"` (see [withOtelCollect()]), the `ExtendedTask` will record
|
||||
#' spans for each invocation of the task. The tracing level at `invoke()` time
|
||||
#' does not affect whether spans are recorded; only the tracing level when
|
||||
#' calling `ExtendedTask$new()` matters.
|
||||
#'
|
||||
#' The OTel span will be named based on the label created from the variable the
|
||||
#' `ExtendedTask` is assigned to. If no label can be determined, the span will
|
||||
#' be named `<anonymous>`. Similar to other Shiny OpenTelemetry spans, the span
|
||||
#' will also include source reference attributes and session ID attributes.
|
||||
#'
|
||||
#' ```r
|
||||
#' withOtelCollect("all", {
|
||||
#' my_task <- ExtendedTask$new(function(...) { ... })
|
||||
#' })
|
||||
#'
|
||||
#' # Span recorded for this invocation: ExtendedTask my_task
|
||||
#' my_task$invoke(...)
|
||||
#' ```
|
||||
#'
|
||||
#' @examplesIf rlang::is_interactive() && rlang::is_installed("mirai")
|
||||
#' library(shiny)
|
||||
#' library(bslib)
|
||||
#' library(mirai)
|
||||
#'
|
||||
#' # Set background processes for running tasks
|
||||
#' daemons(1)
|
||||
#' # Reset when the app is stopped
|
||||
#' onStop(function() daemons(0))
|
||||
#'
|
||||
#' ui <- page_fluid(
|
||||
#' titlePanel("Extended Task Demo"),
|
||||
#' p(
|
||||
#' 'Click the button below to perform a "calculation"',
|
||||
#' "that takes a while to perform."
|
||||
#' ),
|
||||
#' input_task_button("recalculate", "Recalculate"),
|
||||
#' p(textOutput("result"))
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' rand_task <- ExtendedTask$new(function() {
|
||||
#' mirai(
|
||||
#' {
|
||||
#' # Slow operation goes here
|
||||
#' Sys.sleep(2)
|
||||
#' sample(1:100, 1)
|
||||
#' }
|
||||
#' )
|
||||
#' })
|
||||
#'
|
||||
#' # Make button state reflect task.
|
||||
#' # If using R >=4.1, you can do this instead:
|
||||
#' # rand_task <- ExtendedTask$new(...) |> bind_task_button("recalculate")
|
||||
#' bind_task_button(rand_task, "recalculate")
|
||||
#'
|
||||
#' observeEvent(input$recalculate, {
|
||||
#' # Invoke the extended in an observer
|
||||
#' rand_task$invoke()
|
||||
#' })
|
||||
#'
|
||||
#' output$result <- renderText({
|
||||
#' # React to updated results when the task completes
|
||||
#' number <- rand_task$result()
|
||||
#' paste0("Your number is ", number, ".")
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' @export
|
||||
ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
|
||||
public = list(
|
||||
#' @description
|
||||
#' Creates a new `ExtendedTask` object. `ExtendedTask` should generally be
|
||||
#' created either at the top of a server function, or at the top of a module
|
||||
#' server function.
|
||||
#'
|
||||
#' @param func The long-running operation to execute. This should be an
|
||||
#' asynchronous function, meaning, it should use the
|
||||
#' [\{promises\}](https://rstudio.github.io/promises/) package, most
|
||||
#' likely in conjunction with the
|
||||
#' [\{mirai\}](https://mirai.r-lib.org) or
|
||||
#' [\{future\}](https://rstudio.github.io/promises/articles/promises_04_futures.html)
|
||||
#' package. (In short, the return value of `func` should be a
|
||||
#' [`mirai`][mirai::mirai()], [`Future`][future::future()], `promise`,
|
||||
#' or something else that [promises::as.promise()] understands.)
|
||||
#'
|
||||
#' It's also important that this logic does not read from any
|
||||
#' reactive inputs/sources, as inputs may change after the function is
|
||||
#' invoked; instead, if the function needs to access reactive inputs, it
|
||||
#' should take parameters and the caller of the `invoke()` method should
|
||||
#' read reactive inputs and pass them as arguments.
|
||||
initialize = function(func) {
|
||||
private$func <- func
|
||||
|
||||
# Do not show these private reactive values in otel spans
|
||||
with_no_otel_collect({
|
||||
private$rv_status <- reactiveVal("initial", label = "ExtendedTask$private$status")
|
||||
private$rv_value <- reactiveVal(NULL, label = "ExtendedTask$private$value")
|
||||
private$rv_error <- reactiveVal(NULL, label = "ExtendedTask$private$error")
|
||||
})
|
||||
|
||||
private$invocation_queue <- fastmap::fastqueue()
|
||||
|
||||
domain <- getDefaultReactiveDomain()
|
||||
|
||||
# Set a label for the reactive values for easier debugging
|
||||
# Go up an extra sys.call() to get the user's call to ExtendedTask$new()
|
||||
# The first sys.call() is to `initialize(...)`
|
||||
call_srcref <- get_call_srcref(-1)
|
||||
label <- rassignSrcrefToLabel(
|
||||
call_srcref,
|
||||
defaultLabel = "<anonymous>"
|
||||
)
|
||||
private$otel_span_label <- otel_span_label_extended_task(label, domain = domain)
|
||||
private$otel_log_label_add_to_queue <- otel_log_label_extended_task_add_to_queue(label, domain = domain)
|
||||
|
||||
private$otel_attrs <- c(
|
||||
otel_srcref_attributes(call_srcref, "ExtendedTask"),
|
||||
otel_session_id_attrs(domain)
|
||||
) %||% list()
|
||||
|
||||
# Capture this value at init-time, not run-time
|
||||
# This way, the span is only created if otel was enabled at time of creation... just like other spans
|
||||
private$is_recording_otel <- has_otel_collect("reactivity")
|
||||
},
|
||||
#' @description
|
||||
#' Starts executing the long-running operation. If this `ExtendedTask` is
|
||||
#' already running (meaning, a previous call to `invoke()` is not yet
|
||||
#' complete) then enqueues this invocation until after the current
|
||||
#' invocation, and any already-enqueued invocation, completes.
|
||||
#'
|
||||
#' @param ... Parameters to use for this invocation of the underlying
|
||||
#' function. If reactive inputs are needed by the underlying function,
|
||||
#' they should be read by the caller of `invoke` and passed in as
|
||||
#' arguments.
|
||||
invoke = function(...) {
|
||||
args <- rlang::dots_list(..., .ignore_empty = "none")
|
||||
call <- rlang::caller_call(n = 0)
|
||||
|
||||
if (
|
||||
isolate(private$rv_status()) == "running" ||
|
||||
private$invocation_queue$size() > 0
|
||||
) {
|
||||
otel_log(
|
||||
private$otel_log_add_to_queue_label,
|
||||
severity = "debug",
|
||||
attributes = c(
|
||||
private$otel_attrs,
|
||||
list(
|
||||
queue_size = private$invocation_queue$size() + 1L
|
||||
)
|
||||
)
|
||||
)
|
||||
private$invocation_queue$add(list(args = args, call = call))
|
||||
} else {
|
||||
|
||||
if (private$is_recording_otel) {
|
||||
private$otel_span <- start_otel_span(
|
||||
private$otel_span_label,
|
||||
attributes = private$otel_attrs
|
||||
)
|
||||
otel::local_active_span(private$otel_span)
|
||||
}
|
||||
|
||||
private$do_invoke(args, call = call)
|
||||
}
|
||||
invisible(NULL)
|
||||
},
|
||||
#' @description
|
||||
#' This is a reactive read that invalidates the caller when the task's
|
||||
#' status changes.
|
||||
#'
|
||||
#' Returns one of the following values:
|
||||
#'
|
||||
#' * `"initial"`: This `ExtendedTask` has not yet been invoked
|
||||
#' * `"running"`: An invocation is currently running
|
||||
#' * `"success"`: An invocation completed successfully, and a value can be
|
||||
#' retrieved via the `result()` method
|
||||
#' * `"error"`: An invocation completed with an error, which will be
|
||||
#' re-thrown if you call the `result()` method
|
||||
status = function() {
|
||||
private$rv_status()
|
||||
},
|
||||
#' @description
|
||||
#' Attempts to read the results of the most recent invocation. This is a
|
||||
#' reactive read that invalidates as the task's status changes.
|
||||
#'
|
||||
#' The actual behavior differs greatly depending on the current status of
|
||||
#' the task:
|
||||
#'
|
||||
#' * `"initial"`: Throws a silent error (like [`req(FALSE)`][req()]). If
|
||||
#' this happens during output rendering, the output will be blanked out.
|
||||
#' * `"running"`: Throws a special silent error that, if it happens during
|
||||
#' output rendering, makes the output appear "in progress" until further
|
||||
#' notice.
|
||||
#' * `"success"`: Returns the return value of the most recent invocation.
|
||||
#' * `"error"`: Throws whatever error was thrown by the most recent
|
||||
#' invocation.
|
||||
#'
|
||||
#' This method is intended to be called fairly naively by any output or
|
||||
#' reactive expression that cares about the output--you just have to be
|
||||
#' aware that if the result isn't ready for whatever reason, processing will
|
||||
#' stop in much the same way as `req(FALSE)` does, but when the result is
|
||||
#' ready you'll get invalidated, and when you run again the result should be
|
||||
#' there.
|
||||
#'
|
||||
#' Note that the `result()` method is generally not meant to be used with
|
||||
#' [observeEvent()], [eventReactive()], [bindEvent()], or [isolate()] as the
|
||||
#' invalidation will be ignored.
|
||||
result = function() {
|
||||
switch (private$rv_status(),
|
||||
running = req(FALSE, cancelOutput = "progress"),
|
||||
success = if (private$rv_value()$visible) {
|
||||
private$rv_value()$value
|
||||
} else {
|
||||
invisible(private$rv_value()$value)
|
||||
},
|
||||
error = stop(private$rv_error()),
|
||||
# default case (initial, cancelled)
|
||||
req(FALSE)
|
||||
)
|
||||
}
|
||||
),
|
||||
private = list(
|
||||
func = NULL,
|
||||
# reactive value with "initial"|"running"|"success"|"error"
|
||||
rv_status = NULL,
|
||||
rv_value = NULL,
|
||||
rv_error = NULL,
|
||||
invocation_queue = NULL,
|
||||
|
||||
otel_span_label = NULL,
|
||||
otel_log_label_add_to_queue = NULL,
|
||||
otel_attrs = list(),
|
||||
is_recording_otel = FALSE,
|
||||
otel_span = NULL,
|
||||
|
||||
do_invoke = function(args, call = NULL) {
|
||||
private$rv_status("running")
|
||||
private$rv_value(NULL)
|
||||
private$rv_error(NULL)
|
||||
|
||||
p <- promise_resolve(
|
||||
maskReactiveContext(do.call(private$func, args))
|
||||
)
|
||||
|
||||
p <- promises::then(
|
||||
p,
|
||||
onFulfilled = function(value, .visible) {
|
||||
if (is_otel_span(private$otel_span)) {
|
||||
|
||||
private$otel_span$end(status_code = "ok")
|
||||
private$otel_span <- NULL
|
||||
}
|
||||
private$on_success(list(value = value, visible = .visible))
|
||||
},
|
||||
onRejected = function(error) {
|
||||
if (is_otel_span(private$otel_span)) {
|
||||
private$otel_span$end(status_code = "error")
|
||||
private$otel_span <- NULL
|
||||
}
|
||||
private$on_error(error, call = call)
|
||||
}
|
||||
)
|
||||
|
||||
promises::finally(p, onFinally = function() {
|
||||
if (private$invocation_queue$size() > 0) {
|
||||
next_call <- private$invocation_queue$remove()
|
||||
private$do_invoke(next_call$args, next_call$call)
|
||||
}
|
||||
})
|
||||
|
||||
invisible(NULL)
|
||||
},
|
||||
|
||||
on_error = function(err, call = NULL) {
|
||||
cli::cli_warn(
|
||||
"ERROR: An error occurred when invoking the ExtendedTask.",
|
||||
parent = err,
|
||||
call = call
|
||||
)
|
||||
private$rv_status("error")
|
||||
private$rv_error(err)
|
||||
},
|
||||
|
||||
on_success = function(value) {
|
||||
private$rv_status("success")
|
||||
private$rv_value(value)
|
||||
}
|
||||
)
|
||||
)
|
||||
140
R/fileupload.R
Normal file
140
R/fileupload.R
Normal file
@@ -0,0 +1,140 @@
|
||||
# For HTML5-capable browsers, file uploads happen through a series of requests.
|
||||
#
|
||||
# 1. Client tells server that one or more files are about to be uploaded; the
|
||||
# server responds with a "job ID" that the client should use for the rest of
|
||||
# the upload.
|
||||
#
|
||||
# 2. For each file (sequentially):
|
||||
# a. Client tells server the name, size, and type of the file.
|
||||
# b. Client sends server a small-ish blob of data.
|
||||
# c. Repeat 2b until the entire file has been uploaded.
|
||||
# d. Client tells server that the current file is done.
|
||||
#
|
||||
# 3. Repeat 2 until all files have been uploaded.
|
||||
#
|
||||
# 4. Client tells server that all files have been uploaded, along with the
|
||||
# input ID that this data should be associated with.
|
||||
#
|
||||
# Unfortunately this approach will not work for browsers that don't support
|
||||
# HTML5 File API, but the fallback approach we would like to use (multipart
|
||||
# form upload, i.e. traditional HTTP POST-based file upload) doesn't work with
|
||||
# the websockets package's HTTP server at the moment.
|
||||
|
||||
# @description Returns a file's extension, with a leading dot, if one can be
|
||||
# found. A valid extension contains only alphanumeric characters. If there is
|
||||
# no extension, or if it contains non-alphanumeric characters, an empty
|
||||
# string is returned.
|
||||
# @param x character vector giving file paths.
|
||||
# @return The extension of \code{x}, with a leading dot, if one was found.
|
||||
# Otherwise, an empty character vector.
|
||||
maybeGetExtension <- function(x) {
|
||||
ext <- tools::file_ext(x)
|
||||
ifelse(ext == "", ext, paste0(".", ext))
|
||||
}
|
||||
|
||||
FileUploadOperation <- R6Class(
|
||||
'FileUploadOperation',
|
||||
portable = FALSE,
|
||||
class = FALSE,
|
||||
public = list(
|
||||
.parent = NULL,
|
||||
.id = character(0),
|
||||
.files = data.frame(),
|
||||
.dir = character(0),
|
||||
.currentFileInfo = list(),
|
||||
.currentFileData = NULL,
|
||||
.pendingFileInfos = list(),
|
||||
|
||||
initialize = function(parent, id, dir, fileInfos) {
|
||||
.parent <<- parent
|
||||
.id <<- id
|
||||
.files <<- data.frame(name=character(),
|
||||
size=numeric(),
|
||||
type=character(),
|
||||
datapath=character(),
|
||||
stringsAsFactors=FALSE)
|
||||
.dir <<- dir
|
||||
.pendingFileInfos <<- fileInfos
|
||||
},
|
||||
fileBegin = function() {
|
||||
if (length(.pendingFileInfos) < 1)
|
||||
stop("fileBegin called too many times")
|
||||
|
||||
file <- .pendingFileInfos[[1]]
|
||||
.currentFileInfo <<- file
|
||||
.pendingFileInfos <<- tail(.pendingFileInfos, -1)
|
||||
|
||||
fileBasename <- basename(.currentFileInfo$name)
|
||||
filename <- file.path(.dir, paste0(as.character(length(.files$name)), maybeGetExtension(fileBasename)))
|
||||
row <- data.frame(name=fileBasename, size=file$size, type=file$type,
|
||||
datapath=filename, stringsAsFactors=FALSE)
|
||||
|
||||
if (length(.files$name) == 0)
|
||||
.files <<- row
|
||||
else
|
||||
.files <<- rbind(.files, row)
|
||||
|
||||
.currentFileData <<- file(filename, open='wb')
|
||||
},
|
||||
fileChunk = function(rawdata) {
|
||||
writeBin(rawdata, .currentFileData)
|
||||
},
|
||||
fileEnd = function() {
|
||||
close(.currentFileData)
|
||||
},
|
||||
finish = function() {
|
||||
if (length(.pendingFileInfos) > 0)
|
||||
stop("File upload job was stopped prematurely")
|
||||
.parent$onJobFinished(.id)
|
||||
return(.files)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
#' @include map.R
|
||||
FileUploadContext <- R6Class(
|
||||
'FileUploadContext',
|
||||
class = FALSE,
|
||||
private = list(
|
||||
basedir = character(0),
|
||||
operations = 'Map',
|
||||
ids = character(0) # Keep track of all ids used for file uploads
|
||||
),
|
||||
public = list(
|
||||
initialize = function(dir=tempdir()) {
|
||||
private$basedir <- dir
|
||||
private$operations <- Map$new()
|
||||
},
|
||||
createUploadOperation = function(fileInfos) {
|
||||
while (TRUE) {
|
||||
id <- createUniqueId(12)
|
||||
private$ids <- c(private$ids, id)
|
||||
dir <- file.path(private$basedir, id)
|
||||
if (!dir.create(dir))
|
||||
next
|
||||
|
||||
op <- FileUploadOperation$new(self, id, dir, fileInfos)
|
||||
private$operations$set(id, op)
|
||||
return(id)
|
||||
}
|
||||
},
|
||||
getUploadOperation = function(jobId) {
|
||||
private$operations$get(jobId)
|
||||
},
|
||||
onJobFinished = function(jobId) {
|
||||
private$operations$remove(jobId)
|
||||
},
|
||||
# Remove the directories containing file uploads; this is to be called when
|
||||
# a session ends.
|
||||
rmUploadDirs = function() {
|
||||
# Make sure all_paths is underneath the tempdir()
|
||||
if (!grepl(normalizePath(tempdir()), normalizePath(private$basedir), fixed = TRUE)) {
|
||||
stop("Won't remove upload path ", private$basedir,
|
||||
"because it is not under tempdir(): ", tempdir())
|
||||
}
|
||||
|
||||
all_paths <- file.path(private$basedir, private$ids)
|
||||
unlink(all_paths, recursive = TRUE)
|
||||
}
|
||||
)
|
||||
)
|
||||
31
R/globals.R
Normal file
31
R/globals.R
Normal file
@@ -0,0 +1,31 @@
|
||||
# A scope where we can put mutable global state
|
||||
.globals <- new.env(parent = emptyenv())
|
||||
|
||||
.onLoad <- function(libname, pkgname) {
|
||||
# R's lazy-loading package scheme causes the private seed to be cached in the
|
||||
# package itself, making our PRNG completely deterministic. This line resets
|
||||
# the private seed during load.
|
||||
withPrivateSeed(set.seed(NULL))
|
||||
|
||||
for (expr in on_load_exprs) {
|
||||
eval(expr, envir = environment(.onLoad))
|
||||
}
|
||||
|
||||
# Make sure these methods are available to knitr if shiny is loaded but not
|
||||
# attached.
|
||||
s3_register("knitr::knit_print", "reactive")
|
||||
s3_register("knitr::knit_print", "shiny.appobj")
|
||||
s3_register("knitr::knit_print", "shiny.render.function")
|
||||
}
|
||||
|
||||
|
||||
on_load_exprs <- list()
|
||||
# Register an expression to be evaluated when the package is loaded (in the
|
||||
# .onLoad function).
|
||||
on_load <- function(expr) {
|
||||
on_load_exprs[[length(on_load_exprs) + 1]] <<- substitute(expr)
|
||||
}
|
||||
|
||||
on_load({
|
||||
IS_SHINY_LOCAL_PKG <- exists(".__DEVTOOLS__")
|
||||
})
|
||||
543
R/graph.R
Normal file
543
R/graph.R
Normal file
@@ -0,0 +1,543 @@
|
||||
# domain is like session
|
||||
|
||||
|
||||
# used to help define truly global react id's.
|
||||
# should work across session and in global namespace
|
||||
.globals$reactIdCounter <- 0L
|
||||
nextGlobalReactId <- function() {
|
||||
.globals$reactIdCounter <- .globals$reactIdCounter + 1L
|
||||
reactIdStr(.globals$reactIdCounter)
|
||||
}
|
||||
reactIdStr <- function(num) {
|
||||
paste0("r", num)
|
||||
}
|
||||
|
||||
|
||||
#' Reactive Log Visualizer
|
||||
#'
|
||||
#' Provides an interactive browser-based tool for visualizing reactive
|
||||
#' dependencies and execution in your application.
|
||||
#'
|
||||
#' To use the reactive log visualizer, start with a fresh R session and
|
||||
#' run the command `reactlog::reactlog_enable()`; then launch your
|
||||
#' application in the usual way (e.g. using [runApp()]). At
|
||||
#' any time you can hit Ctrl+F3 (or for Mac users, Command+F3) in your
|
||||
#' web browser to launch the reactive log visualization.
|
||||
#'
|
||||
#' The reactive log visualization only includes reactive activity up
|
||||
#' until the time the report was loaded. If you want to see more recent
|
||||
#' activity, refresh the browser.
|
||||
#'
|
||||
#' Note that Shiny does not distinguish between reactive dependencies
|
||||
#' that "belong" to one Shiny user session versus another, so the
|
||||
#' visualization will include all reactive activity that has taken place
|
||||
#' in the process, not just for a particular application or session.
|
||||
#'
|
||||
#' As an alternative to pressing Ctrl/Command+F3--for example, if you
|
||||
#' are using reactives outside of the context of a Shiny
|
||||
#' application--you can run the `reactlogShow` function, which will
|
||||
#' generate the reactive log visualization as a static HTML file and
|
||||
#' launch it in your default browser. In this case, refreshing your
|
||||
#' browser will not load new activity into the report; you will need to
|
||||
#' call `reactlogShow()` explicitly.
|
||||
#'
|
||||
#' For security and performance reasons, do not enable
|
||||
#' `options(shiny.reactlog=TRUE)` (or `reactlog::reactlog_enable()`) in
|
||||
#' production environments. When the option is enabled, it's possible
|
||||
#' for any user of your app to see at least some of the source code of
|
||||
#' your reactive expressions and observers. In addition, reactlog
|
||||
#' should be considered a memory leak as it will constantly grow and
|
||||
#' will never reset until the R session is restarted.
|
||||
#'
|
||||
#' @name reactlog
|
||||
NULL
|
||||
|
||||
|
||||
#' @describeIn reactlog Return a list of reactive information. Can be used in
|
||||
#' conjunction with [reactlog::reactlog_show] to later display the reactlog
|
||||
#' graph.
|
||||
#' @export
|
||||
reactlog <- function() {
|
||||
rLog$asList()
|
||||
}
|
||||
|
||||
#' @describeIn reactlog Display a full reactlog graph for all sessions.
|
||||
#' @param time A boolean that specifies whether or not to display the
|
||||
#' time that each reactive takes to calculate a result.
|
||||
#' @export
|
||||
reactlogShow <- function(time = TRUE) {
|
||||
check_reactlog()
|
||||
reactlog::reactlog_show(reactlog(), time = time)
|
||||
}
|
||||
|
||||
#' @describeIn reactlog Resets the entire reactlog stack. Useful for debugging
|
||||
#' and removing all prior reactive history.
|
||||
#' @export
|
||||
reactlogReset <- function() {
|
||||
rLog$reset()
|
||||
}
|
||||
|
||||
#' @describeIn reactlog Adds "mark" entry into the reactlog stack. This is
|
||||
#' useful for programmatically adding a marked entry in the reactlog, rather
|
||||
#' than using your keyboard's key combination.
|
||||
#'
|
||||
#' For example, we can _mark_ the reactlog at the beginning of an
|
||||
#' `observeEvent`'s calculation:
|
||||
#' ```r
|
||||
#' observeEvent(input$my_event_trigger, {
|
||||
#' # Add a mark in the reactlog
|
||||
#' reactlogAddMark()
|
||||
#' # Run your regular event reaction code here...
|
||||
#' ....
|
||||
#' })
|
||||
#' ```
|
||||
#' @param session The Shiny session to assign the mark to. Defaults to the
|
||||
#' current session.
|
||||
#' @export
|
||||
reactlogAddMark <- function(session = getDefaultReactiveDomain()) {
|
||||
rLog$userMark(session)
|
||||
}
|
||||
|
||||
# called in "/reactlog" middleware
|
||||
renderReactlog <- function(sessionToken = NULL, time = TRUE) {
|
||||
check_reactlog()
|
||||
reactlog::reactlog_render(
|
||||
reactlog(),
|
||||
session_token = sessionToken,
|
||||
time = time
|
||||
)
|
||||
}
|
||||
|
||||
check_reactlog <- function() {
|
||||
if (!is_installed("reactlog", reactlog_min_version)) {
|
||||
rlang::check_installed("reactlog", reactlog_min_version)
|
||||
}
|
||||
}
|
||||
|
||||
# Should match the (suggested) version in DESCRIPTION file
|
||||
reactlog_min_version <- "1.0.0"
|
||||
|
||||
RLog <- R6Class(
|
||||
"RLog",
|
||||
portable = FALSE,
|
||||
private = list(
|
||||
option = "shiny.reactlog",
|
||||
msgOption = "shiny.reactlog.console",
|
||||
appendEntry = function(domain, logEntry) {
|
||||
if (self$isLogging()) {
|
||||
sessionToken <- if (is.null(domain)) NULL else domain$token
|
||||
logStack$push(c(logEntry, list(
|
||||
session = sessionToken,
|
||||
time = as.numeric(Sys.time())
|
||||
)))
|
||||
}
|
||||
if (!is.null(domain)) domain$reactlog(logEntry)
|
||||
}
|
||||
),
|
||||
public = list(
|
||||
msg = "<MessageLogger>",
|
||||
logStack = "<Stack>",
|
||||
noReactIdLabel = "NoCtxReactId",
|
||||
noReactId = reactIdStr("NoCtxReactId"),
|
||||
dummyReactIdLabel = "DummyReactId",
|
||||
dummyReactId = reactIdStr("DummyReactId"),
|
||||
asList = function() {
|
||||
ret <- self$logStack$as_list()
|
||||
attr(ret, "version") <- "1"
|
||||
ret
|
||||
},
|
||||
ctxIdStr = function(ctxId) {
|
||||
if (is.null(ctxId) || identical(ctxId, "")) {
|
||||
return(NULL)
|
||||
}
|
||||
paste0("ctx", ctxId)
|
||||
},
|
||||
namesIdStr = function(reactId) {
|
||||
paste0("names(", reactId, ")")
|
||||
},
|
||||
asListIdStr = function(reactId) {
|
||||
paste0("reactiveValuesToList(", reactId, ")")
|
||||
},
|
||||
asListAllIdStr = function(reactId) {
|
||||
paste0("reactiveValuesToList(", reactId, ", all.names = TRUE)")
|
||||
},
|
||||
keyIdStr = function(reactId, key) {
|
||||
paste0(reactId, "$", key)
|
||||
},
|
||||
valueStr = function(value, n = 200) {
|
||||
if (!self$isLogging()) {
|
||||
# return a placeholder string to avoid calling str
|
||||
return("<reactlog is turned off>")
|
||||
}
|
||||
output <- try(silent = TRUE, {
|
||||
# only capture the first level of the object
|
||||
utils::capture.output(utils::str(value, max.level = 1))
|
||||
})
|
||||
outputTxt <- paste0(output, collapse = "\n")
|
||||
msg$shortenString(outputTxt, n = n)
|
||||
},
|
||||
initialize = function(rlogOption = "shiny.reactlog", msgOption = "shiny.reactlog.console") {
|
||||
private$option <- rlogOption
|
||||
private$msgOption <- msgOption
|
||||
|
||||
self$reset()
|
||||
},
|
||||
reset = function() {
|
||||
.globals$reactIdCounter <- 0L
|
||||
|
||||
self$logStack <- fastmap::faststack()
|
||||
self$msg <- MessageLogger$new(option = private$msgOption)
|
||||
|
||||
# setup dummy and missing react information
|
||||
self$msg$setReact(force = TRUE, list(reactId = self$noReactId, label = self$noReactIdLabel))
|
||||
self$msg$setReact(force = TRUE, list(reactId = self$dummyReactId, label = self$dummyReactIdLabel))
|
||||
},
|
||||
isLogging = function() {
|
||||
isTRUE(getOption(private$option, FALSE))
|
||||
},
|
||||
define = function(reactId, value, label, type, domain) {
|
||||
valueStr <- self$valueStr(value)
|
||||
if (msg$hasReact(reactId)) {
|
||||
stop("react definition for id: ", reactId, " already found!!", "Label: ", label, "Type: ", type)
|
||||
}
|
||||
msg$setReact(list(reactId = reactId, label = label))
|
||||
msg$log("define:", msg$reactStr(reactId), msg$typeStr(type = type), msg$valueStr(valueStr))
|
||||
private$appendEntry(domain, list(
|
||||
action = "define",
|
||||
reactId = reactId,
|
||||
label = msg$shortenString(label),
|
||||
type = type,
|
||||
value = valueStr
|
||||
))
|
||||
},
|
||||
defineNames = function(reactId, value, label, domain) {
|
||||
self$define(self$namesIdStr(reactId), value, self$namesIdStr(label), "reactiveValuesNames", domain)
|
||||
},
|
||||
defineAsList = function(reactId, value, label, domain) {
|
||||
self$define(self$asListIdStr(reactId), value, self$asListIdStr(label), "reactiveValuesAsList", domain)
|
||||
},
|
||||
defineAsListAll = function(reactId, value, label, domain) {
|
||||
self$define(self$asListAllIdStr(reactId), value, self$asListAllIdStr(label), "reactiveValuesAsListAll", domain)
|
||||
},
|
||||
defineKey = function(reactId, value, key, label, domain) {
|
||||
self$define(self$keyIdStr(reactId, key), value, self$keyIdStr(label, key), "reactiveValuesKey", domain)
|
||||
},
|
||||
defineObserver = function(reactId, label, domain) {
|
||||
self$define(reactId, value = NULL, label, "observer", domain)
|
||||
},
|
||||
dependsOn = function(reactId, depOnReactId, ctxId, domain) {
|
||||
if (is.null(reactId)) {
|
||||
return()
|
||||
}
|
||||
ctxId <- ctxIdStr(ctxId)
|
||||
msg$log("dependsOn:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId))
|
||||
private$appendEntry(domain, list(
|
||||
action = "dependsOn",
|
||||
reactId = reactId,
|
||||
depOnReactId = depOnReactId,
|
||||
ctxId = ctxId
|
||||
))
|
||||
},
|
||||
dependsOnKey = function(reactId, depOnReactId, key, ctxId, domain) {
|
||||
self$dependsOn(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain)
|
||||
},
|
||||
dependsOnRemove = function(reactId, depOnReactId, ctxId, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
msg$log("dependsOnRemove:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId))
|
||||
private$appendEntry(domain, list(
|
||||
action = "dependsOnRemove",
|
||||
reactId = reactId,
|
||||
depOnReactId = depOnReactId,
|
||||
ctxId = ctxId
|
||||
))
|
||||
},
|
||||
dependsOnKeyRemove = function(reactId, depOnReactId, key, ctxId, domain) {
|
||||
self$dependsOnRemove(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain)
|
||||
},
|
||||
createContext = function(ctxId, label, type, prevCtxId, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
prevCtxId <- self$ctxIdStr(prevCtxId)
|
||||
msg$log("createContext:", msg$ctxPrevCtxStr(preCtxIdTxt = " ", ctxId, prevCtxId, type))
|
||||
private$appendEntry(domain, list(
|
||||
action = "createContext",
|
||||
ctxId = ctxId,
|
||||
label = msg$shortenString(label),
|
||||
type = type,
|
||||
prevCtxId = prevCtxId,
|
||||
srcref = as.vector(attr(label, "srcref")), srcfile = attr(label, "srcfile")
|
||||
))
|
||||
},
|
||||
enter = function(reactId, ctxId, type, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
if (identical(type, "isolate")) {
|
||||
msg$log("isolateEnter:", msg$reactStr(reactId), msg$ctxStr(ctxId))
|
||||
msg$depthIncrement()
|
||||
private$appendEntry(domain, list(
|
||||
action = "isolateEnter",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId
|
||||
))
|
||||
} else {
|
||||
msg$log("enter:", msg$reactStr(reactId), msg$ctxStr(ctxId, type))
|
||||
msg$depthIncrement()
|
||||
private$appendEntry(domain, list(
|
||||
action = "enter",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId,
|
||||
type = type
|
||||
))
|
||||
}
|
||||
},
|
||||
exit = function(reactId, ctxId, type, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
if (identical(type, "isolate")) {
|
||||
msg$depthDecrement()
|
||||
msg$log("isolateExit:", msg$reactStr(reactId), msg$ctxStr(ctxId))
|
||||
private$appendEntry(domain, list(
|
||||
action = "isolateExit",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId
|
||||
))
|
||||
} else {
|
||||
msg$depthDecrement()
|
||||
msg$log("exit:", msg$reactStr(reactId), msg$ctxStr(ctxId, type))
|
||||
private$appendEntry(domain, list(
|
||||
action = "exit",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId,
|
||||
type = type
|
||||
))
|
||||
}
|
||||
},
|
||||
valueChange = function(reactId, value, domain) {
|
||||
valueStr <- self$valueStr(value)
|
||||
msg$log("valueChange:", msg$reactStr(reactId), msg$valueStr(valueStr))
|
||||
private$appendEntry(domain, list(
|
||||
action = "valueChange",
|
||||
reactId = reactId,
|
||||
value = valueStr
|
||||
))
|
||||
},
|
||||
valueChangeNames = function(reactId, nameValues, domain) {
|
||||
self$valueChange(self$namesIdStr(reactId), nameValues, domain)
|
||||
},
|
||||
valueChangeAsList = function(reactId, listValue, domain) {
|
||||
self$valueChange(self$asListIdStr(reactId), listValue, domain)
|
||||
},
|
||||
valueChangeAsListAll = function(reactId, listValue, domain) {
|
||||
self$valueChange(self$asListAllIdStr(reactId), listValue, domain)
|
||||
},
|
||||
valueChangeKey = function(reactId, key, value, domain) {
|
||||
self$valueChange(self$keyIdStr(reactId, key), value, domain)
|
||||
},
|
||||
invalidateStart = function(reactId, ctxId, type, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
if (identical(type, "isolate")) {
|
||||
msg$log("isolateInvalidateStart:", msg$reactStr(reactId), msg$ctxStr(ctxId))
|
||||
msg$depthIncrement()
|
||||
private$appendEntry(domain, list(
|
||||
action = "isolateInvalidateStart",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId
|
||||
))
|
||||
} else {
|
||||
msg$log("invalidateStart:", msg$reactStr(reactId), msg$ctxStr(ctxId, type))
|
||||
msg$depthIncrement()
|
||||
private$appendEntry(domain, list(
|
||||
action = "invalidateStart",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId,
|
||||
type = type
|
||||
))
|
||||
}
|
||||
},
|
||||
invalidateEnd = function(reactId, ctxId, type, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
if (identical(type, "isolate")) {
|
||||
msg$depthDecrement()
|
||||
msg$log("isolateInvalidateEnd:", msg$reactStr(reactId), msg$ctxStr(ctxId))
|
||||
private$appendEntry(domain, list(
|
||||
action = "isolateInvalidateEnd",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId
|
||||
))
|
||||
} else {
|
||||
msg$depthDecrement()
|
||||
msg$log("invalidateEnd:", msg$reactStr(reactId), msg$ctxStr(ctxId, type))
|
||||
private$appendEntry(domain, list(
|
||||
action = "invalidateEnd",
|
||||
reactId = reactId,
|
||||
ctxId = ctxId,
|
||||
type = type
|
||||
))
|
||||
}
|
||||
},
|
||||
invalidateLater = function(reactId, runningCtx, millis, domain) {
|
||||
msg$log("invalidateLater: ", millis, "ms", msg$reactStr(reactId), msg$ctxStr(runningCtx))
|
||||
private$appendEntry(domain, list(
|
||||
action = "invalidateLater",
|
||||
reactId = reactId,
|
||||
ctxId = runningCtx,
|
||||
millis = millis
|
||||
))
|
||||
},
|
||||
idle = function(domain = NULL) {
|
||||
msg$log("idle")
|
||||
private$appendEntry(domain, list(
|
||||
action = "idle"
|
||||
))
|
||||
},
|
||||
asyncStart = function(domain = NULL) {
|
||||
msg$log("asyncStart")
|
||||
private$appendEntry(domain, list(
|
||||
action = "asyncStart"
|
||||
))
|
||||
},
|
||||
asyncStop = function(domain = NULL) {
|
||||
msg$log("asyncStop")
|
||||
private$appendEntry(domain, list(
|
||||
action = "asyncStop"
|
||||
))
|
||||
},
|
||||
freezeReactiveVal = function(reactId, domain) {
|
||||
msg$log("freeze:", msg$reactStr(reactId))
|
||||
private$appendEntry(domain, list(
|
||||
action = "freeze",
|
||||
reactId = reactId
|
||||
))
|
||||
},
|
||||
freezeReactiveKey = function(reactId, key, domain) {
|
||||
self$freezeReactiveVal(self$keyIdStr(reactId, key), domain)
|
||||
},
|
||||
thawReactiveVal = function(reactId, domain) {
|
||||
msg$log("thaw:", msg$reactStr(reactId))
|
||||
private$appendEntry(domain, list(
|
||||
action = "thaw",
|
||||
reactId = reactId
|
||||
))
|
||||
},
|
||||
thawReactiveKey = function(reactId, key, domain) {
|
||||
self$thawReactiveVal(self$keyIdStr(reactId, key), domain)
|
||||
},
|
||||
userMark = function(domain = NULL) {
|
||||
msg$log("userMark")
|
||||
private$appendEntry(domain, list(
|
||||
action = "userMark"
|
||||
))
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
MessageLogger <- R6Class(
|
||||
"MessageLogger",
|
||||
portable = FALSE,
|
||||
public = list(
|
||||
depth = 0L,
|
||||
reactCache = list(),
|
||||
option = "shiny.reactlog.console",
|
||||
initialize = function(option = "shiny.reactlog.console", depth = 0L) {
|
||||
if (!missing(depth)) self$depth <- depth
|
||||
if (!missing(option)) self$option <- option
|
||||
},
|
||||
isLogging = function() {
|
||||
isTRUE(getOption(self$option))
|
||||
},
|
||||
isNotLogging = function() {
|
||||
!isTRUE(getOption(self$option))
|
||||
},
|
||||
depthIncrement = function() {
|
||||
if (self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
self$depth <- self$depth + 1L
|
||||
},
|
||||
depthDecrement = function() {
|
||||
if (self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
self$depth <- self$depth - 1L
|
||||
},
|
||||
hasReact = function(reactId) {
|
||||
if (self$isNotLogging()) {
|
||||
return(FALSE)
|
||||
}
|
||||
!is.null(self$getReact(reactId))
|
||||
},
|
||||
getReact = function(reactId, force = FALSE) {
|
||||
if (identical(force, FALSE) && self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
self$reactCache[[reactId]]
|
||||
},
|
||||
setReact = function(reactObj, force = FALSE) {
|
||||
if (identical(force, FALSE) && self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
self$reactCache[[reactObj$reactId]] <- reactObj
|
||||
},
|
||||
shortenString = function(txt, n = 250) {
|
||||
if (is.null(txt) || isTRUE(is.na(txt))) {
|
||||
return("")
|
||||
}
|
||||
if (nchar(txt) > n) {
|
||||
return(
|
||||
paste0(substr(txt, 1, n - 3), "...")
|
||||
)
|
||||
}
|
||||
return(txt)
|
||||
},
|
||||
singleLine = function(txt) {
|
||||
gsub("([^\\])\\n", "\\1\\\\n", txt)
|
||||
},
|
||||
valueStr = function(valueStr) {
|
||||
paste0(
|
||||
" '", self$shortenString(self$singleLine(valueStr)), "'"
|
||||
)
|
||||
},
|
||||
reactStr = function(reactId) {
|
||||
if (self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
reactInfo <- self$getReact(reactId)
|
||||
if (is.null(reactInfo)) {
|
||||
return(" <UNKNOWN_REACTID>")
|
||||
}
|
||||
paste0(
|
||||
" ", reactInfo$reactId, ":'", self$shortenString(self$singleLine(reactInfo$label)), "'"
|
||||
)
|
||||
},
|
||||
typeStr = function(type = NULL) {
|
||||
self$ctxStr(ctxId = NULL, type = type)
|
||||
},
|
||||
ctxStr = function(ctxId = NULL, type = NULL) {
|
||||
if (self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
self$ctxPrevCtxStr(ctxId = ctxId, prevCtxId = NULL, type = type)
|
||||
},
|
||||
ctxPrevCtxStr = function(ctxId = NULL, prevCtxId = NULL, type = NULL, preCtxIdTxt = " in ") {
|
||||
if (self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
paste0(
|
||||
if (!is.null(ctxId)) paste0(preCtxIdTxt, ctxId),
|
||||
if (!is.null(prevCtxId)) paste0(" from ", prevCtxId),
|
||||
if (!is.null(type) && !identical(type, "other")) paste0(" - ", type)
|
||||
)
|
||||
},
|
||||
log = function(...) {
|
||||
if (self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
msg <- paste0(
|
||||
paste0(rep("= ", depth), collapse = ""), "- ", paste0(..., collapse = ""),
|
||||
collapse = ""
|
||||
)
|
||||
message(msg)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
on_load({
|
||||
rLog <- RLog$new("shiny.reactlog", "shiny.reactlog.console")
|
||||
})
|
||||
95
R/history.R
Normal file
95
R/history.R
Normal file
@@ -0,0 +1,95 @@
|
||||
|
||||
#' @include reactive-domains.R
|
||||
NULL
|
||||
|
||||
#' @include reactives.R
|
||||
NULL
|
||||
|
||||
#' Get the query string / hash component from the URL
|
||||
#'
|
||||
#' Two user friendly wrappers for getting the query string and the hash
|
||||
#' component from the app's URL.
|
||||
#'
|
||||
#' These can be particularly useful if you want to display different content
|
||||
#' depending on the values in the query string / hash (e.g. instead of basing
|
||||
#' the conditional on an input or a calculated reactive, you can base it on the
|
||||
#' query string). However, note that, if you're changing the query string / hash
|
||||
#' programmatically from within the server code, you must use
|
||||
#' `updateQueryString(_yourNewQueryString_, mode = "push")`. The default
|
||||
#' `mode` for `updateQueryString` is `"replace"`, which doesn't
|
||||
#' raise any events, so any observers or reactives that depend on it will
|
||||
#' *not* get triggered. However, if you're changing the query string / hash
|
||||
#' directly by typing directly in the browser and hitting enter, you don't have
|
||||
#' to worry about this.
|
||||
#'
|
||||
#' @param session A Shiny session object.
|
||||
#'
|
||||
#' @return For `getQueryString`, a named list. For example, the query
|
||||
#' string `?param1=value1¶m2=value2` becomes `list(param1 =
|
||||
#' value1, param2 = value2)`. For `getUrlHash`, a character vector with
|
||||
#' the hash (including the leading `#` symbol).
|
||||
#'
|
||||
#' @seealso [updateQueryString()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ## App 1: getQueryString
|
||||
#' ## Printing the value of the query string
|
||||
#' ## (Use the back and forward buttons to see how the browser
|
||||
#' ## keeps a record of each state)
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' textInput("txt", "Enter new query string"),
|
||||
#' helpText("Format: ?param1=val1¶m2=val2"),
|
||||
#' actionButton("go", "Update"),
|
||||
#' hr(),
|
||||
#' verbatimTextOutput("query")
|
||||
#' ),
|
||||
#' server = function(input, output, session) {
|
||||
#' observeEvent(input$go, {
|
||||
#' updateQueryString(input$txt, mode = "push")
|
||||
#' })
|
||||
#' output$query <- renderText({
|
||||
#' query <- getQueryString()
|
||||
#' queryText <- paste(names(query), query,
|
||||
#' sep = "=", collapse=", ")
|
||||
#' paste("Your query string is:\n", queryText)
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' ## App 2: getUrlHash
|
||||
#' ## Printing the value of the URL hash
|
||||
#' ## (Use the back and forward buttons to see how the browser
|
||||
#' ## keeps a record of each state)
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' textInput("txt", "Enter new hash"),
|
||||
#' helpText("Format: #hash"),
|
||||
#' actionButton("go", "Update"),
|
||||
#' hr(),
|
||||
#' verbatimTextOutput("hash")
|
||||
#' ),
|
||||
#' server = function(input, output, session) {
|
||||
#' observeEvent(input$go, {
|
||||
#' updateQueryString(input$txt, mode = "push")
|
||||
#' })
|
||||
#' output$hash <- renderText({
|
||||
#' hash <- getUrlHash()
|
||||
#' paste("Your hash is:\n", hash)
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#' @export
|
||||
getQueryString <- function(session = getDefaultReactiveDomain()) {
|
||||
parseQueryString(session$clientData$url_search)
|
||||
}
|
||||
|
||||
#' @rdname getQueryString
|
||||
#' @export
|
||||
getUrlHash <- function(session = getDefaultReactiveDomain()) {
|
||||
session$clientData$url_hash
|
||||
}
|
||||
24
R/hooks.R
Normal file
24
R/hooks.R
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
|
||||
# Call an application hook. Application hooks are provided so that front ends
|
||||
# can know when a Shiny application is running:
|
||||
#
|
||||
# shiny.onAppStart -- called when an application begins running
|
||||
# shiny.onAppStop -- called when an appliation stops
|
||||
#
|
||||
# Both hooks are passed the url where the application is accessible (appUrl).
|
||||
# Note that the appUrl can be NULL if the application was run on a UNIX domain
|
||||
# socket rather than a TCP/IP port/
|
||||
callAppHook <- function(name, appUrl) {
|
||||
for (hook in getHooksList(paste0("shiny.", name)))
|
||||
hook(appUrl)
|
||||
}
|
||||
|
||||
# The value for getHook can be a single function or a list of functions,
|
||||
# This function ensures that the result can always be processed as a list
|
||||
getHooksList <- function(name) {
|
||||
hooks <- getHook(name)
|
||||
if (!is.list(hooks))
|
||||
hooks <- list(hooks)
|
||||
hooks
|
||||
}
|
||||
59
R/html-deps.R
Normal file
59
R/html-deps.R
Normal file
@@ -0,0 +1,59 @@
|
||||
#' Create a web dependency
|
||||
#'
|
||||
#' Ensure that a file-based HTML dependency (from the htmltools package) can be
|
||||
#' served over Shiny's HTTP server. This function works by using
|
||||
#' [addResourcePath()] to map the HTML dependency's directory to a
|
||||
#' URL.
|
||||
#'
|
||||
#' @param dependency A single HTML dependency object, created using
|
||||
#' [htmltools::htmlDependency()]. If the `src` value is named,
|
||||
#' then `href` and/or `file` names must be present.
|
||||
#' @param scrubFile If TRUE (the default), remove `src$file` for the
|
||||
#' dependency. This prevents the local file path from being sent to the client
|
||||
#' when dynamic web dependencies are used. If FALSE, don't remove
|
||||
#' `src$file`. Setting it to FALSE should be needed only in very unusual
|
||||
#' cases.
|
||||
#'
|
||||
#' @return A single HTML dependency object that has an `href`-named element
|
||||
#' in its `src`.
|
||||
#' @export
|
||||
createWebDependency <- function(dependency, scrubFile = TRUE) {
|
||||
if (is.null(dependency))
|
||||
return(NULL)
|
||||
|
||||
if (!inherits(dependency, "html_dependency"))
|
||||
stop("Unexpected non-html_dependency type")
|
||||
|
||||
if (is.null(dependency$src$href)) {
|
||||
prefix <- paste(dependency$name, "-", dependency$version, sep = "")
|
||||
addResourcePath(prefix, dependency$src$file)
|
||||
dependency$src$href <- prefix
|
||||
}
|
||||
|
||||
# Don't leak local file path to client
|
||||
if (scrubFile)
|
||||
dependency$src$file <- NULL
|
||||
|
||||
return(dependency)
|
||||
}
|
||||
|
||||
|
||||
# Given a Shiny tag object, process singletons and dependencies. Returns a list
|
||||
# with rendered HTML and dependency objects.
|
||||
# This implementation is very similar to renderTags(), but ignores
|
||||
# <head> handling (it should only be used after the user session has started)
|
||||
processDeps <- function(tags, session) {
|
||||
tags <- utils::getFromNamespace("tagify", "htmltools")(tags)
|
||||
ui <- takeSingletons(tags, session$singletons, desingleton = FALSE)$ui
|
||||
ui <- surroundSingletons(ui)
|
||||
dependencies <- lapply(
|
||||
resolveDependencies(findDependencies(ui, tagify = FALSE)),
|
||||
createWebDependency
|
||||
)
|
||||
names(dependencies) <- NULL
|
||||
|
||||
list(
|
||||
html = doRenderTags(ui),
|
||||
deps = dependencies
|
||||
)
|
||||
}
|
||||
141
R/image-interact-opts.R
Normal file
141
R/image-interact-opts.R
Normal file
@@ -0,0 +1,141 @@
|
||||
#' Control interactive plot point events
|
||||
#'
|
||||
#' These functions give control over the `click`, `dblClick` and
|
||||
#' `hover` events generated by [imageOutput()] and [plotOutput()].
|
||||
#'
|
||||
#' @param id Input value name. For example, if the value is `"plot_click"`,
|
||||
#' then the event data will be available as `input$plot_click`.
|
||||
#' @param clip Should the click area be clipped to the plotting area? If
|
||||
#' `FALSE`, then the server will receive click events even when the mouse is
|
||||
#' outside the plotting area, as long as it is still inside the image.
|
||||
#' @param delay For `dblClickOpts()`: the maximum delay (in ms) between a
|
||||
#' pair clicks for them to be counted as a double-click.
|
||||
#'
|
||||
#' For `hoverOpts()`: how long to delay (in ms) when debouncing or throttling
|
||||
#' before sending the mouse location to the server.
|
||||
#' @param delayType The type of algorithm for limiting the number of hover
|
||||
#' events. Use `"throttle"` to limit the number of hover events to one
|
||||
#' every `delay` milliseconds. Use `"debounce"` to suspend events
|
||||
#' while the cursor is moving, and wait until the cursor has been at rest for
|
||||
#' `delay` milliseconds before sending an event.
|
||||
#' @seealso [brushOpts()] for brushing events.
|
||||
#' @export
|
||||
clickOpts <- function(id, clip = TRUE) {
|
||||
if (is.null(id))
|
||||
stop("id must not be NULL")
|
||||
|
||||
list(
|
||||
id = id,
|
||||
clip = clip
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
#' @rdname clickOpts
|
||||
dblclickOpts <- function(id, clip = TRUE, delay = 400) {
|
||||
if (is.null(id))
|
||||
stop("id must not be NULL")
|
||||
|
||||
list(
|
||||
id = id,
|
||||
clip = clip,
|
||||
delay = delay
|
||||
)
|
||||
}
|
||||
|
||||
#' @param nullOutside If `TRUE` (the default), the value will be set to
|
||||
#' `NULL` when the mouse exits the plotting area. If `FALSE`, the
|
||||
#' value will stop changing when the cursor exits the plotting area.
|
||||
#' @export
|
||||
#' @rdname clickOpts
|
||||
hoverOpts <- function(id, delay = 300,
|
||||
delayType = c("debounce", "throttle"), clip = TRUE,
|
||||
nullOutside = TRUE) {
|
||||
if (is.null(id))
|
||||
stop("id must not be NULL")
|
||||
|
||||
list(
|
||||
id = id,
|
||||
delay = delay,
|
||||
delayType = match.arg(delayType),
|
||||
clip = clip,
|
||||
nullOutside = nullOutside
|
||||
)
|
||||
}
|
||||
|
||||
#' Create an object representing brushing options
|
||||
#'
|
||||
#' This generates an object representing brushing options, to be passed as the
|
||||
#' `brush` argument of [imageOutput()] or
|
||||
#' [plotOutput()].
|
||||
#'
|
||||
#' @param id Input value name. For example, if the value is `"plot_brush"`,
|
||||
#' then the coordinates will be available as `input$plot_brush`. Multiple
|
||||
#' `imageOutput`/`plotOutput` calls may share the same `id`
|
||||
#' value; brushing one image or plot will cause any other brushes with the
|
||||
#' same `id` to disappear.
|
||||
#' @param fill Fill color of the brush. If `'auto'`, it derives from the link
|
||||
#' color of the plot's HTML container (if **thematic** is enabled, and `accent`
|
||||
#' is a non-`'auto'` value, that color is used instead).
|
||||
#' @param stroke Outline color of the brush. If `'auto'`, it derives from the
|
||||
#' foreground color of the plot's HTML container (if **thematic** is enabled,
|
||||
#' and `fg` is a non-`'auto'` value, that color is used instead).
|
||||
#' @param opacity Opacity of the brush
|
||||
#' @param delay How long to delay (in milliseconds) when debouncing or
|
||||
#' throttling, before sending the brush data to the server.
|
||||
#' @param delayType The type of algorithm for limiting the number of brush
|
||||
#' events. Use `"throttle"` to limit the number of brush events to one
|
||||
#' every `delay` milliseconds. Use `"debounce"` to suspend events
|
||||
#' while the cursor is moving, and wait until the cursor has been at rest for
|
||||
#' `delay` milliseconds before sending an event.
|
||||
#' @param clip Should the brush area be clipped to the plotting area? If FALSE,
|
||||
#' then the user will be able to brush outside the plotting area, as long as
|
||||
#' it is still inside the image.
|
||||
#' @param direction The direction for brushing. If `"xy"`, the brush can be
|
||||
#' drawn and moved in both x and y directions. If `"x"`, or `"y"`,
|
||||
#' the brush wil work horizontally or vertically.
|
||||
#' @param resetOnNew When a new image is sent to the browser (via
|
||||
#' [renderImage()]), should the brush be reset? The default,
|
||||
#' `FALSE`, is useful if you want to update the plot while keeping the
|
||||
#' brush. Using `TRUE` is useful if you want to clear the brush whenever
|
||||
#' the plot is updated.
|
||||
#' @seealso [clickOpts()] for clicking events.
|
||||
#' @export
|
||||
brushOpts <- function(id, fill = "#9cf", stroke = "#036",
|
||||
opacity = 0.25, delay = 300,
|
||||
delayType = c("debounce", "throttle"), clip = TRUE,
|
||||
direction = c("xy", "x", "y"),
|
||||
resetOnNew = FALSE) {
|
||||
if (is.null(id))
|
||||
stop("id must not be NULL")
|
||||
|
||||
if (identical(fill, "auto")) {
|
||||
fill <- getThematicOption("accent", "auto")
|
||||
}
|
||||
if (identical(stroke, "auto")) {
|
||||
stroke <- getThematicOption("fg", "auto")
|
||||
}
|
||||
|
||||
list(
|
||||
id = id,
|
||||
fill = fill,
|
||||
stroke = stroke,
|
||||
opacity = opacity,
|
||||
delay = delay,
|
||||
delayType = match.arg(delayType),
|
||||
clip = clip,
|
||||
direction = match.arg(direction),
|
||||
resetOnNew = resetOnNew
|
||||
)
|
||||
}
|
||||
|
||||
getThematicOption <- function(name = "", default = NULL, resolve = FALSE) {
|
||||
if (isNamespaceLoaded("thematic")) {
|
||||
# TODO: use :: once thematic is on CRAN
|
||||
tgo <- utils::getFromNamespace("thematic_get_option", "thematic")
|
||||
tgo(name = name, default = default, resolve = resolve)
|
||||
} else {
|
||||
default
|
||||
}
|
||||
}
|
||||
532
R/image-interact.R
Normal file
532
R/image-interact.R
Normal file
@@ -0,0 +1,532 @@
|
||||
#' Find rows of data selected on an interactive plot.
|
||||
#'
|
||||
#' @description
|
||||
#' `brushedPoints()` returns rows from a data frame which are under a brush.
|
||||
#' `nearPoints()` returns rows from a data frame which are near a click, hover,
|
||||
#' or double-click. Alternatively, set `allRows = TRUE` to return all rows from
|
||||
#' the input data with an additional column `selected_` that indicates which
|
||||
#' rows of the would be selected.
|
||||
#'
|
||||
#' @section ggplot2:
|
||||
#' For plots created with ggplot2, it is not necessary to specify the
|
||||
#' column names to `xvar`, `yvar`, `panelvar1`, and `panelvar2` as that
|
||||
#' information can be automatically derived from the plot specification.
|
||||
#'
|
||||
#' Note, however, that this will not work if you use a computed column, like
|
||||
#' `aes(speed/2, dist))`. Instead, we recommend that you modify the data
|
||||
#' first, and then make the plot with "raw" columns in the modified data.
|
||||
#'
|
||||
#' @section Brushing:
|
||||
#' If x or y column is a factor, then it will be coerced to an integer vector.
|
||||
#' If it is a character vector, then it will be coerced to a factor and then
|
||||
#' integer vector. This means that the brush will be considered to cover a
|
||||
#' given character/factor value when it covers the center value.
|
||||
#'
|
||||
#' If the brush is operating in just the x or y directions (e.g., with
|
||||
#' `brushOpts(direction = "x")`, then this function will filter out points
|
||||
#' using just the x or y variable, whichever is appropriate.
|
||||
#'
|
||||
#' @returns
|
||||
#' A data frame based on `df`, containing the observations selected by the
|
||||
#' brush or near the click event. For `nearPoints()`, the rows will be sorted
|
||||
#' by distance to the event.
|
||||
#'
|
||||
#' If `allRows = TRUE`, then all rows will returned, along with a new
|
||||
#' `selected_` column that indicates whether or not the point was selected.
|
||||
#' The output from `nearPoints()` will no longer be sorted, but you can
|
||||
#' set `addDist = TRUE` to get an additional column that gives the pixel
|
||||
#' distance to the pointer.
|
||||
#'
|
||||
#' @param df A data frame from which to select rows.
|
||||
#' @param brush,coordinfo The data from a brush or click/dblclick/hover event
|
||||
#' e.g. `input$plot_brush`, `input$plot_click`.
|
||||
#' @param xvar,yvar A string giving the name of the variable on the x or y axis.
|
||||
#' These are only required for base graphics, and must be the name of
|
||||
#' a column in `df`.
|
||||
#' @param panelvar1,panelvar2 A string giving the name of a panel variable.
|
||||
#' For expert use only; in most cases these will be automatically
|
||||
#' derived from the ggplot2 spec.
|
||||
#' @param allRows If `FALSE` (the default) return a data frame containing
|
||||
#' the selected rows. If `TRUE`, the input data frame will have a new
|
||||
#' column, `selected_`, which indicates whether the row was selected or not.
|
||||
#' @param threshold A maximum distance (in pixels) to the pointer location.
|
||||
#' Rows in the data frame will be selected if the distance to the pointer is
|
||||
#' less than `threshold`.
|
||||
#' @param maxpoints Maximum number of rows to return. If `NULL` (the default),
|
||||
#' will return all rows within the threshold distance.
|
||||
#' @param addDist If TRUE, add a column named `dist_` that contains the
|
||||
#' distance from the coordinate to the point, in pixels. When no pointer
|
||||
#' event has yet occurred, the value of `dist_` will be `NA`.
|
||||
#' @seealso [plotOutput()] for example usage.
|
||||
#' @export
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # Note that in practice, these examples would need to go in reactives
|
||||
#' # or observers.
|
||||
#'
|
||||
#' # This would select all points within 5 pixels of the click
|
||||
#' nearPoints(mtcars, input$plot_click)
|
||||
#'
|
||||
#' # Select just the nearest point within 10 pixels of the click
|
||||
#' nearPoints(mtcars, input$plot_click, threshold = 10, maxpoints = 1)
|
||||
#'
|
||||
#' }
|
||||
brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
|
||||
panelvar1 = NULL, panelvar2 = NULL,
|
||||
allRows = FALSE) {
|
||||
if (is.null(brush)) {
|
||||
if (allRows)
|
||||
df$selected_ <- FALSE
|
||||
else
|
||||
df <- df[0, , drop = FALSE]
|
||||
|
||||
return(df)
|
||||
}
|
||||
|
||||
if (is.null(brush$xmin)) {
|
||||
stop("brushedPoints requires a brush object with xmin, xmax, ymin, and ymax.")
|
||||
}
|
||||
|
||||
# Which direction(s) the brush is selecting over. Direction can be 'x', 'y',
|
||||
# or 'xy'.
|
||||
use_x <- grepl("x", brush$direction)
|
||||
use_y <- grepl("y", brush$direction)
|
||||
|
||||
# We transitioned to using %||% in Shiny 1.6.0. Previously, these vars could
|
||||
# be NA, because the old %OR% operator recognized NA. These warnings and
|
||||
# the NULL replacement are here just to ease the transition in case anyone is
|
||||
# using NA. We can remove these checks in a future version of Shiny.
|
||||
# https://github.com/rstudio/shiny/pull/3172
|
||||
if (is_na(xvar)) { xvar <- NULL; warning("xvar should be NULL, not NA.") }
|
||||
if (is_na(yvar)) { yvar <- NULL; warning("yvar should be NULL, not NA.") }
|
||||
if (is_na(panelvar1)) { panelvar1 <- NULL; warning("panelvar1 should be NULL, not NA.") }
|
||||
if (is_na(panelvar2)) { panelvar2 <- NULL; warning("panelvar2 should be NULL, not NA.") }
|
||||
|
||||
# Try to extract vars from brush object
|
||||
xvar <- xvar %||% brush$mapping$x
|
||||
yvar <- yvar %||% brush$mapping$y
|
||||
panelvar1 <- panelvar1 %||% brush$mapping$panelvar1
|
||||
panelvar2 <- panelvar2 %||% brush$mapping$panelvar2
|
||||
|
||||
# Filter out x and y values
|
||||
keep_rows <- rep(TRUE, nrow(df))
|
||||
if (use_x) {
|
||||
if (is.null(xvar))
|
||||
stop("brushedPoints: not able to automatically infer `xvar` from brush")
|
||||
if (!(xvar %in% names(df)))
|
||||
stop("brushedPoints: `xvar` ('", xvar ,"') not in names of input")
|
||||
keep_rows <- keep_rows & within_brush(df[[xvar]], brush, "x")
|
||||
}
|
||||
if (use_y) {
|
||||
if (is.null(yvar))
|
||||
stop("brushedPoints: not able to automatically infer `yvar` from brush")
|
||||
if (!(yvar %in% names(df)))
|
||||
stop("brushedPoints: `yvar` ('", yvar ,"') not in names of input")
|
||||
keep_rows <- keep_rows & within_brush(df[[yvar]], brush, "y")
|
||||
}
|
||||
|
||||
# Find which rows are matches for the panel vars (if present)
|
||||
if (!is.null(panelvar1))
|
||||
keep_rows <- keep_rows & panelMatch(brush$panelvar1, df[[panelvar1]])
|
||||
if (!is.null(panelvar2))
|
||||
keep_rows <- keep_rows & panelMatch(brush$panelvar2, df[[panelvar2]])
|
||||
|
||||
if (allRows) {
|
||||
df$selected_ <- keep_rows
|
||||
df
|
||||
} else {
|
||||
df[keep_rows, , drop = FALSE]
|
||||
}
|
||||
}
|
||||
|
||||
# The `brush` data structure will look something like the examples below.
|
||||
# For base graphics, `mapping` is empty, and there are no panelvars:
|
||||
# List of 8
|
||||
# $ xmin : num 3.73
|
||||
# $ xmax : num 4.22
|
||||
# $ ymin : num 13.9
|
||||
# $ ymax : num 19.8
|
||||
# $ coords_css:List of 4
|
||||
# ..$ xmin: int 260
|
||||
# ..$ xmax: int 298
|
||||
# ..$ ymin: num 112
|
||||
# ..$ ymax: num 205
|
||||
# $ coords_img:List of 4
|
||||
# ..$ xmin: int 325
|
||||
# ..$ xmax: num 372
|
||||
# ..$ ymin: num 140
|
||||
# ..$ ymax: num 257
|
||||
# $ img_css_ratio:List of 2
|
||||
# ..$ x: num 1.25
|
||||
# ..$ y: num 1.25
|
||||
# $ mapping: Named list()
|
||||
# $ domain :List of 4
|
||||
# ..$ left : num 1.36
|
||||
# ..$ right : num 5.58
|
||||
# ..$ bottom: num 9.46
|
||||
# ..$ top : num 34.8
|
||||
# $ range :List of 4
|
||||
# ..$ left : num 58
|
||||
# ..$ right : num 429
|
||||
# ..$ bottom: num 226
|
||||
# ..$ top : num 58
|
||||
# $ log :List of 2
|
||||
# ..$ x: NULL
|
||||
# ..$ y: NULL
|
||||
# $ direction: chr "y"
|
||||
#
|
||||
# For ggplot2, the mapping vars usually will be included, and if faceting is
|
||||
# used, they will be listed as panelvars:
|
||||
# List of 10
|
||||
# $ xmin : num 3.18
|
||||
# $ xmax : num 3.78
|
||||
# $ ymin : num 17.1
|
||||
# $ ymax : num 20.4
|
||||
# $ panelvar1: chr "6"
|
||||
# $ panelvar2: chr "0
|
||||
# $ coords_css:List of 4
|
||||
# ..$ xmin: int 260
|
||||
# ..$ xmax: int 298
|
||||
# ..$ ymin: num 112
|
||||
# ..$ ymax: num 205
|
||||
# $ coords_img:List of 4
|
||||
# ..$ xmin: int 325
|
||||
# ..$ xmax: num 372
|
||||
# ..$ ymin: num 140
|
||||
# ..$ ymax: num 257
|
||||
# $ img_css_ratio:List of 2
|
||||
# ..$ x: num 1.25
|
||||
# ..$ y: num 1.25
|
||||
# $ mapping :List of 4
|
||||
# ..$ x : chr "wt"
|
||||
# ..$ y : chr "mpg"
|
||||
# ..$ panelvar1: chr "cyl"
|
||||
# ..$ panelvar2: chr "am"
|
||||
# $ domain :List of 4
|
||||
# ..$ left : num 1.32
|
||||
# ..$ right : num 5.62
|
||||
# ..$ bottom: num 9.22
|
||||
# ..$ top : num 35.1
|
||||
# $ range :List of 4
|
||||
# ..$ left : num 172
|
||||
# ..$ right : num 300
|
||||
# ..$ bottom: num 144
|
||||
# ..$ top : num 28.5
|
||||
# $ log :List of 2
|
||||
# ..$ x: NULL
|
||||
# ..$ y: NULL
|
||||
# $ direction: chr "y"
|
||||
|
||||
|
||||
#' @export
|
||||
#' @rdname brushedPoints
|
||||
nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
|
||||
panelvar1 = NULL, panelvar2 = NULL,
|
||||
threshold = 5, maxpoints = NULL, addDist = FALSE,
|
||||
allRows = FALSE) {
|
||||
if (is.null(coordinfo)) {
|
||||
if (addDist)
|
||||
df$dist_ <- NA_real_
|
||||
|
||||
if (allRows)
|
||||
df$selected_ <- FALSE
|
||||
else
|
||||
df <- df[0, , drop = FALSE]
|
||||
|
||||
return(df)
|
||||
}
|
||||
|
||||
if (is.null(coordinfo$x)) {
|
||||
stop("nearPoints requires a click/hover/double-click object with x and y values.")
|
||||
}
|
||||
|
||||
# We transitioned to using %||% in Shiny 1.6.0. Previously, these vars could
|
||||
# be NA, because the old %OR% operator recognized NA. These warnings and
|
||||
# the NULL replacement are here just to ease the transition in case anyone is
|
||||
# using NA. We can remove these checks in a future version of Shiny.
|
||||
# https://github.com/rstudio/shiny/pull/3172
|
||||
if (is_na(xvar)) { xvar <- NULL; warning("xvar should be NULL, not NA.") }
|
||||
if (is_na(yvar)) { yvar <- NULL; warning("yvar should be NULL, not NA.") }
|
||||
if (is_na(panelvar1)) { panelvar1 <- NULL; warning("panelvar1 should be NULL, not NA.") }
|
||||
if (is_na(panelvar2)) { panelvar2 <- NULL; warning("panelvar2 should be NULL, not NA.") }
|
||||
|
||||
# Try to extract vars from coordinfo object
|
||||
xvar <- xvar %||% coordinfo$mapping$x
|
||||
yvar <- yvar %||% coordinfo$mapping$y
|
||||
panelvar1 <- panelvar1 %||% coordinfo$mapping$panelvar1
|
||||
panelvar2 <- panelvar2 %||% coordinfo$mapping$panelvar2
|
||||
|
||||
if (is.null(xvar))
|
||||
stop("nearPoints: not able to automatically infer `xvar` from coordinfo")
|
||||
if (is.null(yvar))
|
||||
stop("nearPoints: not able to automatically infer `yvar` from coordinfo")
|
||||
|
||||
if (!(xvar %in% names(df)))
|
||||
stop("nearPoints: `xvar` ('", xvar ,"') not in names of input")
|
||||
if (!(yvar %in% names(df)))
|
||||
stop("nearPoints: `yvar` ('", yvar ,"') not in names of input")
|
||||
|
||||
# Extract data values from the data frame
|
||||
coordinfo <- fortifyDiscreteLimits(coordinfo)
|
||||
x <- asNumber(df[[xvar]], coordinfo$domain$discrete_limits$x)
|
||||
y <- asNumber(df[[yvar]], coordinfo$domain$discrete_limits$y)
|
||||
|
||||
# Get the coordinates of the point (in img pixel coordinates)
|
||||
point_img <- coordinfo$coords_img
|
||||
|
||||
# Get coordinates of data points (in img pixel coordinates)
|
||||
data_img <- scaleCoords(x, y, coordinfo)
|
||||
|
||||
# Get x/y distances (in css coordinates)
|
||||
dist_css <- list(
|
||||
x = (data_img$x - point_img$x) / coordinfo$img_css_ratio$x,
|
||||
y = (data_img$y - point_img$y) / coordinfo$img_css_ratio$y
|
||||
)
|
||||
|
||||
# Distances of data points to the target point, in css pixels.
|
||||
dists <- sqrt(dist_css$x^2 + dist_css$y^2)
|
||||
|
||||
if (addDist)
|
||||
df$dist_ <- dists
|
||||
|
||||
keep_rows <- (dists <= threshold)
|
||||
|
||||
# Find which rows are matches for the panel vars (if present)
|
||||
if (!is.null(panelvar1))
|
||||
keep_rows <- keep_rows & panelMatch(coordinfo$panelvar1, df[[panelvar1]])
|
||||
if (!is.null(panelvar2))
|
||||
keep_rows <- keep_rows & panelMatch(coordinfo$panelvar2, df[[panelvar2]])
|
||||
|
||||
# Track the indices to keep
|
||||
keep_idx <- which(keep_rows)
|
||||
|
||||
# Order by distance
|
||||
dists <- dists[keep_idx]
|
||||
keep_idx <- keep_idx[order(dists)]
|
||||
|
||||
# Keep max number of rows
|
||||
if (!is.null(maxpoints) && length(keep_idx) > maxpoints) {
|
||||
keep_idx <- keep_idx[seq_len(maxpoints)]
|
||||
}
|
||||
|
||||
if (allRows) {
|
||||
# Add selected_ column if needed
|
||||
df$selected_ <- FALSE
|
||||
df$selected_[keep_idx] <- TRUE
|
||||
|
||||
} else {
|
||||
# If we don't keep all rows, return just the selected rows, sorted by
|
||||
# distance.
|
||||
df <- df[keep_idx, , drop = FALSE]
|
||||
}
|
||||
|
||||
df
|
||||
}
|
||||
|
||||
# The coordinfo data structure will look something like the examples below.
|
||||
# For base graphics, `mapping` is empty, and there are no panelvars:
|
||||
# List of 7
|
||||
# $ x : num 4.37
|
||||
# $ y : num 12
|
||||
# $ coords_css:List of 2
|
||||
# ..$ x: int 286
|
||||
# ..$ y: int 192
|
||||
# $ coords_img:List of 2
|
||||
# ..$ x: num 358
|
||||
# ..$ y: int 240
|
||||
# $ img_css_ratio:List of 2
|
||||
# ..$ x: num 1.25
|
||||
# ..$ y: num 1.25
|
||||
# $ mapping : Named list()
|
||||
# $ domain :List of 4
|
||||
# ..$ left : num 1.36
|
||||
# ..$ right : num 5.58
|
||||
# ..$ bottom: num 9.46
|
||||
# ..$ top : num 34.8
|
||||
# $ range :List of 4
|
||||
# ..$ left : num 58
|
||||
# ..$ right : num 429
|
||||
# ..$ bottom: num 226
|
||||
# ..$ top : num 58
|
||||
# $ log :List of 2
|
||||
# ..$ x: NULL
|
||||
# ..$ y: NULL
|
||||
# $ .nonce : num 0.343
|
||||
#
|
||||
# For ggplot2, the mapping vars usually will be included, and if faceting is
|
||||
# used, they will be listed as panelvars:
|
||||
# List of 9
|
||||
# $ x : num 3.78
|
||||
# $ y : num 17.1
|
||||
# $ coords_css:List of 2
|
||||
# ..$ x: int 286
|
||||
# ..$ y: int 192
|
||||
# $ coords_img:List of 2
|
||||
# ..$ x: num 358
|
||||
# ..$ y: int 240
|
||||
# $ img_css_ratio:List of 2
|
||||
# ..$ x: num 1.25
|
||||
# ..$ y: num 1.25
|
||||
# $ panelvar1 : chr "6"
|
||||
# $ panelvar2 : chr "0"
|
||||
# $ mapping :List of 4
|
||||
# ..$ x : chr "wt"
|
||||
# ..$ y : chr "mpg"
|
||||
# ..$ panelvar1: chr "cyl"
|
||||
# ..$ panelvar2: chr "am"
|
||||
# $ domain :List of 4
|
||||
# ..$ left : num 1.32
|
||||
# ..$ right : num 5.62
|
||||
# ..$ bottom: num 9.22
|
||||
# ..$ top : num 35.1
|
||||
# $ range :List of 4
|
||||
# ..$ left : num 172
|
||||
# ..$ right : num 300
|
||||
# ..$ bottom: num 144
|
||||
# ..$ top : num 28.5
|
||||
# $ log :List of 2
|
||||
# ..$ x: NULL
|
||||
# ..$ y: NULL
|
||||
# $ .nonce : num 0.603
|
||||
|
||||
# Helper to determine if data values are within the limits of
|
||||
# an input brush
|
||||
within_brush <- function(vals, brush, var = "x") {
|
||||
var <- match.arg(var, c("x", "y"))
|
||||
brush <- fortifyDiscreteLimits(brush)
|
||||
vals <- asNumber(vals, brush$domain$discrete_limits[[var]])
|
||||
# It's possible for a non-missing data values to not
|
||||
# map to the axis limits, for example:
|
||||
# https://github.com/rstudio/shiny/pull/2410#issuecomment-488100881
|
||||
!is.na(vals) &
|
||||
vals >= brush[[paste0(var, "min")]] &
|
||||
vals <= brush[[paste0(var, "max")]]
|
||||
}
|
||||
|
||||
# Coerce various types of variables to numbers. This works for Date, POSIXt,
|
||||
# characters, and factors. Used because the mouse coords are numeric.
|
||||
# The `levels` argument should be used when mapping this variable to
|
||||
# a known set of discrete levels, which is needed for ggplot2 since
|
||||
# it allows you to control ordering and possible values of a discrete
|
||||
# positional scale (#2410)
|
||||
asNumber <- function(x, levels = NULL) {
|
||||
if (length(levels)) return(match(x, levels))
|
||||
if (is.character(x)) x <- as.factor(x)
|
||||
if (is.factor(x)) x <- as.integer(x)
|
||||
as.numeric(x)
|
||||
}
|
||||
|
||||
# Ensure the discrete limits/levels of a coordmap received
|
||||
# from the client matches the data structure sent the client.
|
||||
#
|
||||
# When we construct the coordmap (in getGgplotCoordmap()),
|
||||
# we save a character vector which may contain missing values
|
||||
# (e.g., c("a", "b", NA)). When that same character is received
|
||||
# from the client, it runs through decodeMessage() which sets
|
||||
# simplifyVector=FALSE, which means NA are replaced by NULL
|
||||
# (because jsonlite::fromJSON('["a", "b", null]') -> list("a", "b", NULL))
|
||||
#
|
||||
# Thankfully, it doesn't seem like it's meaningful for limits to
|
||||
# contains a NULL in the 1st place, so we simply treat NULL like NA.
|
||||
# For more context, https://github.com/rstudio/shiny/issues/2666
|
||||
fortifyDiscreteLimits <- function(coord) {
|
||||
# Note that discrete_limits$x/y are populated iff
|
||||
# x/y are discrete mappings
|
||||
coord$domain$discrete_limits <- lapply(
|
||||
coord$domain$discrete_limits,
|
||||
function(var) {
|
||||
# if there is an 'explicit' NULL, then the limits are NA
|
||||
if (is.null(var)) return(NA)
|
||||
vapply(var, function(x) {
|
||||
if (is.null(x) || isTRUE(is.na(x))) NA_character_ else x
|
||||
}, character(1))
|
||||
}
|
||||
)
|
||||
coord
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Given a panelvar value and a vector x, return logical vector indicating which
|
||||
# items match the panelvar value. Because the panelvar value is always a
|
||||
# string but the vector could be numeric, it might be necessary to coerce the
|
||||
# panelvar to a number before comparing to the vector.
|
||||
panelMatch <- function(search_value, x) {
|
||||
if (is.null(search_value)) return(is.na(x))
|
||||
if (is.numeric(x)) search_value <- as.numeric(search_value)
|
||||
x == search_value
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Scaling functions
|
||||
# These functions have direct analogs in Javascript code, except these are
|
||||
# vectorized for x and y.
|
||||
|
||||
# Map a value x from a domain to a range. If clip is true, clip it to the
|
||||
# range.
|
||||
mapLinear <- function(x, domainMin, domainMax, rangeMin, rangeMax, clip = TRUE) {
|
||||
factor <- (rangeMax - rangeMin) / (domainMax - domainMin)
|
||||
val <- x - domainMin
|
||||
newval <- (val * factor) + rangeMin
|
||||
|
||||
if (clip) {
|
||||
maxval <- max(rangeMax, rangeMin)
|
||||
minval <- min(rangeMax, rangeMin)
|
||||
newval[newval > maxval] <- maxval
|
||||
newval[newval < minval] <- minval
|
||||
}
|
||||
newval
|
||||
}
|
||||
|
||||
# Scale val from domain to range. If logbase is present, use log scaling.
|
||||
scale1D <- function(val, domainMin, domainMax, rangeMin, rangeMax,
|
||||
logbase = NULL, clip = TRUE) {
|
||||
if (!is.null(logbase))
|
||||
val <- log(val, logbase)
|
||||
mapLinear(val, domainMin, domainMax, rangeMin, rangeMax, clip)
|
||||
}
|
||||
|
||||
# Inverse scale val, from range to domain. If logbase is present, use inverse
|
||||
# log (power) transformation.
|
||||
scaleInv1D <- function(val, domainMin, domainMax, rangeMin, rangeMax,
|
||||
logbase = NULL, clip = TRUE) {
|
||||
res <- mapLinear(val, rangeMin, rangeMax, domainMin, domainMax, clip)
|
||||
if (!is.null(logbase))
|
||||
res <- logbase ^ res
|
||||
res
|
||||
}
|
||||
|
||||
# Scale x and y coordinates from domain to range, using information in
|
||||
# scaleinfo. scaleinfo must contain items $domain, $range, and $log. The
|
||||
# scaleinfo object corresponds to one element from the coordmap object generated
|
||||
# by getPrevPlotCoordmap or getGgplotCoordmap; it is the scaling information for
|
||||
# one panel in a plot.
|
||||
scaleCoords <- function(x, y, scaleinfo) {
|
||||
if (is.null(scaleinfo))
|
||||
return(NULL)
|
||||
|
||||
domain <- scaleinfo$domain
|
||||
range <- scaleinfo$range
|
||||
log <- scaleinfo$log
|
||||
|
||||
list(
|
||||
x = scale1D(x, domain$left, domain$right, range$left, range$right, log$x),
|
||||
y = scale1D(y, domain$bottom, domain$top, range$bottom, range$top, log$y)
|
||||
)
|
||||
}
|
||||
|
||||
# Inverse scale x and y coordinates from range to domain, using information in
|
||||
# scaleinfo.
|
||||
scaleInvCoords <- function(x, y, scaleinfo) {
|
||||
if (is.null(scaleinfo))
|
||||
return(NULL)
|
||||
|
||||
domain <- scaleinfo$domain
|
||||
range <- scaleinfo$range
|
||||
log <- scaleinfo$log
|
||||
|
||||
list(
|
||||
x = scaleInv1D(x, domain$left, domain$right, range$left, range$right, log$x),
|
||||
y = scaleInv1D(y, domain$bottom, domain$top, range$bottom, range$top, log$y)
|
||||
)
|
||||
}
|
||||
132
R/imageutils.R
Normal file
132
R/imageutils.R
Normal file
@@ -0,0 +1,132 @@
|
||||
startPNG <- function(filename, width, height, res, ...) {
|
||||
pngfun <- if ((getOption('shiny.useragg') %||% TRUE) && is_installed("ragg")) {
|
||||
ragg::agg_png
|
||||
} else if (capabilities("aqua")) {
|
||||
# i.e., png(type = 'quartz')
|
||||
grDevices::png
|
||||
} else if ((getOption('shiny.usecairo') %||% TRUE) && is_installed("Cairo")) {
|
||||
Cairo::CairoPNG
|
||||
} else {
|
||||
# i.e., png(type = 'cairo')
|
||||
grDevices::png
|
||||
}
|
||||
|
||||
args <- list2(filename = filename, width = width, height = height, res = res, ...)
|
||||
|
||||
# It's possible for width/height to be NULL/numeric(0) (e.g., when using
|
||||
# suspendWhenHidden=F w/ tabsetPanel(), see rstudio/shiny#1409), so when
|
||||
# this happens let the device determine what the default size should be.
|
||||
if (length(args$width) == 0) args$width <- NULL
|
||||
if (length(args$height) == 0) args$height <- NULL
|
||||
|
||||
# Set a smarter default for the device's bg argument (based on thematic's global state).
|
||||
# Note that, technically, this is really only needed for CairoPNG, since the other
|
||||
# devices allow their bg arg to be overridden by par(bg=...), which thematic does prior
|
||||
# to plot-time, but it shouldn't hurt to inform other the device directly as well
|
||||
if (is.null(args$bg) && isNamespaceLoaded("thematic")) {
|
||||
args$bg <- getThematicOption("bg", "white")
|
||||
# auto vals aren't resolved until plot time, so if we see one, resolve it
|
||||
if (isTRUE("auto" == args$bg)) {
|
||||
args$bg <- getCurrentOutputInfo()[["bg"]]()
|
||||
}
|
||||
}
|
||||
|
||||
# Handle both bg and background device arg
|
||||
# https://github.com/r-lib/ragg/issues/35
|
||||
fmls <- names(formals(pngfun))
|
||||
if (("background" %in% fmls) && (!"bg" %in% fmls)) {
|
||||
if (is.null(args$background)) {
|
||||
args$background <- args$bg
|
||||
}
|
||||
args$bg <- NULL
|
||||
}
|
||||
|
||||
do.call(pngfun, args)
|
||||
# Call plot.new() so that even if no plotting operations are performed at
|
||||
# least we have a blank background. N.B. we need to set the margin to 0
|
||||
# temporarily before plot.new() because when the plot size is small (e.g.
|
||||
# 200x50), we will get an error "figure margin too large", which is triggered
|
||||
# by plot.new() with the default (large) margin. However, this does not
|
||||
# guarantee user's code in func() will not trigger the error -- they may have
|
||||
# to set par(mar = smaller_value) before they draw base graphics.
|
||||
op <- graphics::par(mar = rep(0, 4))
|
||||
tryCatch(
|
||||
graphics::plot.new(),
|
||||
finally = graphics::par(op)
|
||||
)
|
||||
|
||||
grDevices::dev.cur()
|
||||
}
|
||||
|
||||
#' Capture a plot as a PNG file.
|
||||
#'
|
||||
#' The PNG graphics device used is determined in the following order:
|
||||
#' * If the ragg package is installed (and the `shiny.useragg` is not
|
||||
#' set to `FALSE`), then use [ragg::agg_png()].
|
||||
#' * If a quartz device is available (i.e., `capabilities("aqua")` is
|
||||
#' `TRUE`), then use `png(type = "quartz")`.
|
||||
#' * If the Cairo package is installed (and the `shiny.usecairo` option
|
||||
#' is not set to `FALSE`), then use [Cairo::CairoPNG()].
|
||||
#' * Otherwise, use [grDevices::png()]. In this case, Linux and Windows
|
||||
#' may not antialias some point shapes, resulting in poor quality output.
|
||||
#'
|
||||
#' @details
|
||||
#' A `NULL` value provided to `width` or `height` is ignored (i.e., the
|
||||
#' default `width` or `height` of the graphics device is used).
|
||||
#'
|
||||
#' @param func A function that generates a plot.
|
||||
#' @param filename The name of the output file. Defaults to a temp file with
|
||||
#' extension `.png`.
|
||||
#' @param width Width in pixels.
|
||||
#' @param height Height in pixels.
|
||||
#' @param res Resolution in pixels per inch. This value is passed to the
|
||||
#' graphics device. Note that this affects the resolution of PNG rendering in
|
||||
#' R; it won't change the actual ppi of the browser.
|
||||
#' @param ... Arguments to be passed through to the graphics device. These can
|
||||
#' be used to set the width, height, background color, etc.
|
||||
#'
|
||||
#' @return A path to the newly generated PNG file.
|
||||
#'
|
||||
#' @export
|
||||
plotPNG <- function(func, filename=tempfile(fileext='.png'),
|
||||
width=400, height=400, res=72, ...) {
|
||||
dv <- startPNG(filename, width, height, res, ...)
|
||||
on.exit(grDevices::dev.off(dv), add = TRUE)
|
||||
func()
|
||||
|
||||
filename
|
||||
}
|
||||
|
||||
createGraphicsDevicePromiseDomain <- function(which = dev.cur()) {
|
||||
force(which)
|
||||
|
||||
new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
force(onFulfilled)
|
||||
function(...) {
|
||||
old <- dev.cur()
|
||||
dev.set(which)
|
||||
on.exit(dev.set(old))
|
||||
|
||||
onFulfilled(...)
|
||||
}
|
||||
},
|
||||
wrapOnRejected = function(onRejected) {
|
||||
force(onRejected)
|
||||
function(...) {
|
||||
old <- dev.cur()
|
||||
dev.set(which)
|
||||
on.exit(dev.set(old))
|
||||
|
||||
onRejected(...)
|
||||
}
|
||||
},
|
||||
wrapSync = function(expr) {
|
||||
old <- dev.cur()
|
||||
dev.set(which)
|
||||
on.exit(dev.set(old))
|
||||
|
||||
force(expr)
|
||||
}
|
||||
)
|
||||
}
|
||||
122
R/input-action.R
Normal file
122
R/input-action.R
Normal file
@@ -0,0 +1,122 @@
|
||||
#' Action button/link
|
||||
#'
|
||||
#' Creates an action button or link whose value is initially zero, and increments by one
|
||||
#' each time it is pressed.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param label The contents of the button or link--usually a text label, but
|
||||
#' you could also use any other HTML, like an image.
|
||||
#' @param icon An optional [icon()] to appear on the button.
|
||||
#' @param disabled If `TRUE`, the button will not be clickable. Use
|
||||
#' [updateActionButton()] to dynamically enable/disable the button.
|
||||
#' @param ... Named attributes to be applied to the button or link.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sliderInput("obs", "Number of observations", 0, 1000, 500),
|
||||
#' actionButton("goButton", "Go!", class = "btn-success"),
|
||||
#' plotOutput("distPlot")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' # Take a dependency on input$goButton. This will run once initially,
|
||||
#' # because the value changes from NULL to 0.
|
||||
#' input$goButton
|
||||
#'
|
||||
#' # Use isolate() to avoid dependency on input$obs
|
||||
#' dist <- isolate(rnorm(input$obs))
|
||||
#' hist(dist)
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' ## Example of adding extra class values
|
||||
#' actionButton("largeButton", "Large Primary Button", class = "btn-primary btn-lg")
|
||||
#' actionLink("infoLink", "Information Link", class = "btn-info")
|
||||
#'
|
||||
#' @seealso [observeEvent()] and [eventReactive()]
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' An integer of class `"shinyActionButtonValue"`. This class differs from
|
||||
#' ordinary integers in that a value of 0 is considered "falsy".
|
||||
#' This implies two things:
|
||||
#' * Event handlers (e.g., [observeEvent()], [eventReactive()]) won't execute on initial load.
|
||||
#' * Input validation (e.g., [req()], [need()]) will fail on initial load.
|
||||
#' @export
|
||||
actionButton <- function(inputId, label, icon = NULL, width = NULL,
|
||||
disabled = FALSE, ...) {
|
||||
|
||||
value <- restoreInput(id = inputId, default = NULL)
|
||||
|
||||
icon <- validateIcon(icon)
|
||||
|
||||
if (!is.null(icon)) {
|
||||
icon <- span(icon, class = "action-icon")
|
||||
}
|
||||
|
||||
if (!is.null(label)) {
|
||||
label <- span(label, class = "action-label")
|
||||
}
|
||||
|
||||
tags$button(
|
||||
id = inputId,
|
||||
style = css(width = validateCssUnit(width)),
|
||||
type = "button",
|
||||
class = "btn btn-default action-button",
|
||||
`data-val` = value,
|
||||
disabled = if (isTRUE(disabled)) NA else NULL,
|
||||
icon, label,
|
||||
...
|
||||
)
|
||||
}
|
||||
|
||||
#' @rdname actionButton
|
||||
#' @export
|
||||
actionLink <- function(inputId, label, icon = NULL, ...) {
|
||||
value <- restoreInput(id = inputId, default = NULL)
|
||||
|
||||
icon <- validateIcon(icon)
|
||||
|
||||
if (!is.null(icon)) {
|
||||
icon <- span(icon, class = "action-icon")
|
||||
}
|
||||
|
||||
if (!is.null(label)) {
|
||||
label <- span(label, class = "action-label")
|
||||
}
|
||||
|
||||
tags$a(
|
||||
id = inputId,
|
||||
href = "#",
|
||||
class = "action-button action-link",
|
||||
`data-val` = value,
|
||||
icon, label,
|
||||
...
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# Throw an informative warning if icon isn't html-ish
|
||||
validateIcon <- function(icon) {
|
||||
if (length(icon) == 0) {
|
||||
return(icon)
|
||||
}
|
||||
if (!isTagLike(icon)) {
|
||||
rlang::warn(
|
||||
c(
|
||||
"It appears that a non-HTML value was provided to `icon`.",
|
||||
i = "Try using a `shiny::icon()` (or an equivalent) to get an icon."
|
||||
),
|
||||
class = "shiny-validate-icon"
|
||||
)
|
||||
}
|
||||
icon
|
||||
}
|
||||
44
R/input-checkbox.R
Normal file
44
R/input-checkbox.R
Normal file
@@ -0,0 +1,44 @@
|
||||
#' Checkbox Input Control
|
||||
#'
|
||||
#' Create a checkbox that can be used to specify logical values.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param value Initial value (`TRUE` or `FALSE`).
|
||||
#' @return A checkbox control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [checkboxGroupInput()], [updateCheckboxInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' checkboxInput("somevalue", "Some value", FALSE),
|
||||
#' verbatimTextOutput("value")
|
||||
#' )
|
||||
#' server <- function(input, output) {
|
||||
#' output$value <- renderText({ input$somevalue })
|
||||
#' }
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' `TRUE` if checked, `FALSE` otherwise.
|
||||
#'
|
||||
#' @export
|
||||
checkboxInput <- function(inputId, label, value = FALSE, width = NULL) {
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
inputTag <- tags$input(id = inputId, type="checkbox", class = "shiny-input-checkbox")
|
||||
if (!is.null(value) && value)
|
||||
inputTag$attribs$checked <- "checked"
|
||||
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
div(class = "checkbox",
|
||||
tags$label(inputTag, tags$span(label))
|
||||
)
|
||||
)
|
||||
}
|
||||
107
R/input-checkboxgroup.R
Normal file
107
R/input-checkboxgroup.R
Normal file
@@ -0,0 +1,107 @@
|
||||
#' Checkbox Group Input Control
|
||||
#'
|
||||
#' Create a group of checkboxes that can be used to toggle multiple choices
|
||||
#' independently. The server will receive the input as a character vector of the
|
||||
#' selected values.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param choices List of values to show checkboxes for. If elements of the list
|
||||
#' are named then that name rather than the value is displayed to the user. If
|
||||
#' this argument is provided, then `choiceNames` and `choiceValues`
|
||||
#' must not be provided, and vice-versa. The values should be strings; other
|
||||
#' types (such as logicals and numbers) will be coerced to strings.
|
||||
#' @param selected The values that should be initially selected, if any.
|
||||
#' @param inline If `TRUE`, render the choices inline (i.e. horizontally)
|
||||
#' @param choiceNames,choiceValues List of names and values, respectively,
|
||||
#' that are displayed to the user in the app and correspond to the each
|
||||
#' choice (for this reason, `choiceNames` and `choiceValues`
|
||||
#' must have the same length). If either of these arguments is
|
||||
#' provided, then the other *must* be provided and `choices`
|
||||
#' *must not* be provided. The advantage of using both of these over
|
||||
#' a named list for `choices` is that `choiceNames` allows any
|
||||
#' type of UI object to be passed through (tag objects, icons, HTML code,
|
||||
#' ...), instead of just simple text. See Examples.
|
||||
#'
|
||||
#' @return A list of HTML elements that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [checkboxInput()], [updateCheckboxGroupInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' checkboxGroupInput("variable", "Variables to show:",
|
||||
#' c("Cylinders" = "cyl",
|
||||
#' "Transmission" = "am",
|
||||
#' "Gears" = "gear")),
|
||||
#' tableOutput("data")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' output$data <- renderTable({
|
||||
#' mtcars[, c("mpg", input$variable), drop = FALSE]
|
||||
#' }, rownames = TRUE)
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' checkboxGroupInput("icons", "Choose icons:",
|
||||
#' choiceNames =
|
||||
#' list(icon("calendar"), icon("bed"),
|
||||
#' icon("cog"), icon("bug")),
|
||||
#' choiceValues =
|
||||
#' list("calendar", "bed", "cog", "bug")
|
||||
#' ),
|
||||
#' textOutput("txt")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' output$txt <- renderText({
|
||||
#' icons <- paste(input$icons, collapse = ", ")
|
||||
#' paste("You chose", icons)
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @section Server value:
|
||||
#' Character vector of values corresponding to the boxes that are checked.
|
||||
#'
|
||||
#' @export
|
||||
checkboxGroupInput <- function(inputId, label, choices = NULL, selected = NULL,
|
||||
inline = FALSE, width = NULL, choiceNames = NULL, choiceValues = NULL) {
|
||||
|
||||
# keep backward compatibility with Shiny < 1.0.1 (see #1649)
|
||||
if (is.null(choices) && is.null(choiceNames) && is.null(choiceValues)) {
|
||||
choices <- character(0)
|
||||
}
|
||||
|
||||
args <- normalizeChoicesArgs(choices, choiceNames, choiceValues)
|
||||
|
||||
selected <- restoreInput(id = inputId, default = selected)
|
||||
|
||||
# default value if it's not specified
|
||||
if (!is.null(selected)) selected <- as.character(selected)
|
||||
|
||||
options <- generateOptions(inputId, selected, inline,
|
||||
'checkbox', args$choiceNames, args$choiceValues)
|
||||
|
||||
divClass <- "form-group shiny-input-checkboxgroup shiny-input-container"
|
||||
if (inline)
|
||||
divClass <- paste(divClass, "shiny-input-container-inline")
|
||||
|
||||
# return label and select tag
|
||||
inputLabel <- shinyInputLabel(inputId, label)
|
||||
tags$div(id = inputId,
|
||||
style = css(width = validateCssUnit(width)),
|
||||
class = divClass,
|
||||
# https://www.w3.org/TR/wai-aria-practices/examples/checkbox/checkbox-1/checkbox-1.html
|
||||
role = "group",
|
||||
`aria-labelledby` = inputLabel$attribs$id,
|
||||
inputLabel,
|
||||
options
|
||||
)
|
||||
}
|
||||
180
R/input-date.R
Normal file
180
R/input-date.R
Normal file
@@ -0,0 +1,180 @@
|
||||
#' Create date input
|
||||
#'
|
||||
#' Creates a text input which, when clicked on, brings up a calendar that
|
||||
#' the user can click on to select dates.
|
||||
#'
|
||||
#' The date `format` string specifies how the date will be displayed in
|
||||
#' the browser. It allows the following values:
|
||||
#'
|
||||
#' \itemize{
|
||||
#' \item `yy` Year without century (12)
|
||||
#' \item `yyyy` Year with century (2012)
|
||||
#' \item `mm` Month number, with leading zero (01-12)
|
||||
#' \item `m` Month number, without leading zero (1-12)
|
||||
#' \item `M` Abbreviated month name
|
||||
#' \item `MM` Full month name
|
||||
#' \item `dd` Day of month with leading zero
|
||||
#' \item `d` Day of month without leading zero
|
||||
#' \item `D` Abbreviated weekday name
|
||||
#' \item `DD` Full weekday name
|
||||
#' }
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param value The starting date. Either a Date object, or a string in
|
||||
#' `yyyy-mm-dd` format. If NULL (the default), will use the current date
|
||||
#' in the client's time zone.
|
||||
#' @param min The minimum allowed date. Either a Date object, or a string in
|
||||
#' `yyyy-mm-dd` format.
|
||||
#' @param max The maximum allowed date. Either a Date object, or a string in
|
||||
#' `yyyy-mm-dd` format.
|
||||
#' @param format The format of the date to display in the browser. Defaults to
|
||||
#' `"yyyy-mm-dd"`.
|
||||
#' @param startview The date range shown when the input object is first clicked.
|
||||
#' Can be "month" (the default), "year", or "decade".
|
||||
#' @param weekstart Which day is the start of the week. Should be an integer
|
||||
#' from 0 (Sunday) to 6 (Saturday).
|
||||
#' @param language The language used for month and day names. Default is "en".
|
||||
#' Other valid values include "ar", "az", "bg", "bs", "ca", "cs", "cy", "da",
|
||||
#' "de", "el", "en-AU", "en-GB", "eo", "es", "et", "eu", "fa", "fi", "fo",
|
||||
#' "fr-CH", "fr", "gl", "he", "hr", "hu", "hy", "id", "is", "it-CH", "it",
|
||||
#' "ja", "ka", "kh", "kk", "ko", "kr", "lt", "lv", "me", "mk", "mn", "ms",
|
||||
#' "nb", "nl-BE", "nl", "no", "pl", "pt-BR", "pt", "ro", "rs-latin", "rs",
|
||||
#' "ru", "sk", "sl", "sq", "sr-latin", "sr", "sv", "sw", "th", "tr", "uk",
|
||||
#' "vi", "zh-CN", and "zh-TW".
|
||||
#' @param autoclose Whether or not to close the datepicker immediately when a
|
||||
#' date is selected.
|
||||
#' @param datesdisabled Which dates should be disabled. Either a Date object,
|
||||
#' or a string in `yyyy-mm-dd` format.
|
||||
#' @param daysofweekdisabled Days of the week that should be disabled. Should be
|
||||
#' a integer vector with values from 0 (Sunday) to 6 (Saturday).
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [dateRangeInput()], [updateDateInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' dateInput("date1", "Date:", value = "2012-02-29"),
|
||||
#'
|
||||
#' # Default value is the date in client's time zone
|
||||
#' dateInput("date2", "Date:"),
|
||||
#'
|
||||
#' # value is always yyyy-mm-dd, even if the display format is different
|
||||
#' dateInput("date3", "Date:", value = "2012-02-29", format = "mm/dd/yy"),
|
||||
#'
|
||||
#' # Pass in a Date object
|
||||
#' dateInput("date4", "Date:", value = Sys.Date()-10),
|
||||
#'
|
||||
#' # Use different language and different first day of week
|
||||
#' dateInput("date5", "Date:",
|
||||
#' language = "ru",
|
||||
#' weekstart = 1),
|
||||
#'
|
||||
#' # Start with decade view instead of default month view
|
||||
#' dateInput("date6", "Date:",
|
||||
#' startview = "decade"),
|
||||
#'
|
||||
#' # Disable Mondays and Tuesdays.
|
||||
#' dateInput("date7", "Date:", daysofweekdisabled = c(1,2)),
|
||||
#'
|
||||
#' # Disable specific dates.
|
||||
#' dateInput("date8", "Date:", value = "2012-02-29",
|
||||
#' datesdisabled = c("2012-03-01", "2012-03-02"))
|
||||
#' )
|
||||
#'
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A [Date] vector of length 1.
|
||||
#'
|
||||
#' @export
|
||||
dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
|
||||
format = "yyyy-mm-dd", startview = "month", weekstart = 0,
|
||||
language = "en", width = NULL, autoclose = TRUE,
|
||||
datesdisabled = NULL, daysofweekdisabled = NULL) {
|
||||
|
||||
value <- dateYMD(value, "value")
|
||||
min <- dateYMD(min, "min")
|
||||
max <- dateYMD(max, "max")
|
||||
datesdisabled <- dateYMD(datesdisabled, "datesdisabled")
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
tags$div(id = inputId,
|
||||
class = "shiny-date-input form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$input(type = "text",
|
||||
class = "form-control",
|
||||
# `aria-labelledby` attribute is required for accessibility to avoid doubled labels (#2951).
|
||||
`aria-labelledby` = paste0(inputId, "-label"),
|
||||
# title attribute is announced for screen readers for date format.
|
||||
title = paste("Date format:", format),
|
||||
`data-date-language` = language,
|
||||
`data-date-week-start` = weekstart,
|
||||
`data-date-format` = format,
|
||||
`data-date-start-view` = startview,
|
||||
`data-min-date` = min,
|
||||
`data-max-date` = max,
|
||||
`data-initial-date` = value,
|
||||
`data-date-autoclose` = if (autoclose) "true" else "false",
|
||||
`data-date-dates-disabled` =
|
||||
# Ensure NULL is not sent as `{}` but as 'null'
|
||||
jsonlite::toJSON(datesdisabled, null = 'null'),
|
||||
`data-date-days-of-week-disabled` =
|
||||
jsonlite::toJSON(daysofweekdisabled, null = 'null')
|
||||
),
|
||||
datePickerDependency()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
datePickerDependency <- function(theme) {
|
||||
list(
|
||||
htmlDependency(
|
||||
name = "bootstrap-datepicker-js",
|
||||
version = version_bs_date_picker,
|
||||
src = "www/shared/datepicker",
|
||||
package = "shiny",
|
||||
script = if (getOption("shiny.minified", TRUE)) "js/bootstrap-datepicker.min.js"
|
||||
else "js/bootstrap-datepicker.js",
|
||||
# Need to enable noConflict mode. See #1346.
|
||||
head = "<script>(function() {
|
||||
var datepicker = $.fn.datepicker.noConflict();
|
||||
$.fn.bsDatepicker = datepicker;
|
||||
})();
|
||||
</script>"
|
||||
),
|
||||
bslib::bs_dependency_defer(datePickerCSS)
|
||||
)
|
||||
}
|
||||
|
||||
datePickerSass <- function() {
|
||||
sass::sass_file(
|
||||
system_file(package = "shiny", "www/shared/datepicker/scss/build3.scss")
|
||||
)
|
||||
}
|
||||
|
||||
datePickerCSS <- function(theme) {
|
||||
if (!is_bs_theme(theme)) {
|
||||
return(htmlDependency(
|
||||
name = "bootstrap-datepicker-css",
|
||||
version = version_bs_date_picker,
|
||||
src = "www/shared/datepicker",
|
||||
package = "shiny",
|
||||
stylesheet = "css/bootstrap-datepicker3.min.css"
|
||||
))
|
||||
}
|
||||
|
||||
bslib::bs_dependency(
|
||||
input = datePickerSass(),
|
||||
theme = theme,
|
||||
name = "bootstrap-datepicker",
|
||||
version = version_bs_date_picker,
|
||||
cache_key_extra = get_package_version("shiny")
|
||||
)
|
||||
}
|
||||
142
R/input-daterange.R
Normal file
142
R/input-daterange.R
Normal file
@@ -0,0 +1,142 @@
|
||||
#' Create date range input
|
||||
#'
|
||||
#' Creates a pair of text inputs which, when clicked on, bring up calendars that
|
||||
#' the user can click on to select dates.
|
||||
#'
|
||||
#' The date `format` string specifies how the date will be displayed in
|
||||
#' the browser. It allows the following values:
|
||||
#'
|
||||
#' \itemize{
|
||||
#' \item `yy` Year without century (12)
|
||||
#' \item `yyyy` Year with century (2012)
|
||||
#' \item `mm` Month number, with leading zero (01-12)
|
||||
#' \item `m` Month number, without leading zero (1-12)
|
||||
#' \item `M` Abbreviated month name
|
||||
#' \item `MM` Full month name
|
||||
#' \item `dd` Day of month with leading zero
|
||||
#' \item `d` Day of month without leading zero
|
||||
#' \item `D` Abbreviated weekday name
|
||||
#' \item `DD` Full weekday name
|
||||
#' }
|
||||
#'
|
||||
#' @inheritParams dateInput
|
||||
#' @param start The initial start date. Either a Date object, or a string in
|
||||
#' `yyyy-mm-dd` format. If NULL (the default), will use the current
|
||||
#' date in the client's time zone.
|
||||
#' @param end The initial end date. Either a Date object, or a string in
|
||||
#' `yyyy-mm-dd` format. If NULL (the default), will use the current
|
||||
#' date in the client's time zone.
|
||||
#' @param separator String to display between the start and end input boxes.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [dateInput()], [updateDateRangeInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' dateRangeInput("daterange1", "Date range:",
|
||||
#' start = "2001-01-01",
|
||||
#' end = "2010-12-31"),
|
||||
#'
|
||||
#' # Default start and end is the current date in the client's time zone
|
||||
#' dateRangeInput("daterange2", "Date range:"),
|
||||
#'
|
||||
#' # start and end are always specified in yyyy-mm-dd, even if the display
|
||||
#' # format is different
|
||||
#' dateRangeInput("daterange3", "Date range:",
|
||||
#' start = "2001-01-01",
|
||||
#' end = "2010-12-31",
|
||||
#' min = "2001-01-01",
|
||||
#' max = "2012-12-21",
|
||||
#' format = "mm/dd/yy",
|
||||
#' separator = " - "),
|
||||
#'
|
||||
#' # Pass in Date objects
|
||||
#' dateRangeInput("daterange4", "Date range:",
|
||||
#' start = Sys.Date()-10,
|
||||
#' end = Sys.Date()+10),
|
||||
#'
|
||||
#' # Use different language and different first day of week
|
||||
#' dateRangeInput("daterange5", "Date range:",
|
||||
#' language = "de",
|
||||
#' weekstart = 1),
|
||||
#'
|
||||
#' # Start with decade view instead of default month view
|
||||
#' dateRangeInput("daterange6", "Date range:",
|
||||
#' startview = "decade")
|
||||
#' )
|
||||
#'
|
||||
#' shinyApp(ui, server = function(input, output) { })
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A [Date] vector of length 2.
|
||||
#'
|
||||
#' @export
|
||||
dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
|
||||
min = NULL, max = NULL, format = "yyyy-mm-dd", startview = "month",
|
||||
weekstart = 0, language = "en", separator = " to ", width = NULL,
|
||||
autoclose = TRUE) {
|
||||
|
||||
start <- dateYMD(start, "start")
|
||||
end <- dateYMD(end, "end")
|
||||
min <- dateYMD(min, "min")
|
||||
max <- dateYMD(max, "max")
|
||||
|
||||
restored <- restoreInput(id = inputId, default = list(start, end))
|
||||
start <- restored[[1]]
|
||||
end <- restored[[2]]
|
||||
|
||||
attachDependencies(
|
||||
div(id = inputId,
|
||||
class = "shiny-date-range-input form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
|
||||
shinyInputLabel(inputId, label),
|
||||
# input-daterange class is needed for dropdown behavior
|
||||
div(class = "input-daterange input-group input-group-sm",
|
||||
tags$input(
|
||||
class = "form-control",
|
||||
type = "text",
|
||||
# `aria-labelledby` attribute is required for accessibility to avoid doubled labels (#2951).
|
||||
`aria-labelledby` = paste0(inputId, "-label"),
|
||||
# title attribute is announced for screen readers for date format.
|
||||
title = paste("Date format:", format),
|
||||
`data-date-language` = language,
|
||||
`data-date-week-start` = weekstart,
|
||||
`data-date-format` = format,
|
||||
`data-date-start-view` = startview,
|
||||
`data-min-date` = min,
|
||||
`data-max-date` = max,
|
||||
`data-initial-date` = start,
|
||||
`data-date-autoclose` = if (autoclose) "true" else "false"
|
||||
),
|
||||
# input-group-prepend and input-group-append are for bootstrap 4 forward compat
|
||||
span(class = "input-group-addon input-group-prepend input-group-append",
|
||||
span(class = "input-group-text",
|
||||
separator
|
||||
)
|
||||
),
|
||||
tags$input(
|
||||
class = "form-control",
|
||||
type = "text",
|
||||
# `aria-labelledby` attribute is required for accessibility to avoid doubled labels (#2951).
|
||||
`aria-labelledby` = paste0(inputId, "-label"),
|
||||
# title attribute is announced for screen readers for date format.
|
||||
title = paste("Date format:", format),
|
||||
`data-date-language` = language,
|
||||
`data-date-week-start` = weekstart,
|
||||
`data-date-format` = format,
|
||||
`data-date-start-view` = startview,
|
||||
`data-min-date` = min,
|
||||
`data-max-date` = max,
|
||||
`data-initial-date` = end,
|
||||
`data-date-autoclose` = if (autoclose) "true" else "false"
|
||||
)
|
||||
)
|
||||
),
|
||||
datePickerDependency()
|
||||
)
|
||||
}
|
||||
151
R/input-file.R
Normal file
151
R/input-file.R
Normal file
@@ -0,0 +1,151 @@
|
||||
#' File Upload Control
|
||||
#'
|
||||
#' Create a file upload control that can be used to upload one or more files.
|
||||
#'
|
||||
#' Whenever a file upload completes, the corresponding input variable is set to
|
||||
#' a dataframe. See the `Server value` section.
|
||||
#'
|
||||
#' Each time files are uploaded, they are written to a new random subdirectory
|
||||
#' inside of R's process-level temporary directory. The Shiny user session keeps
|
||||
#' track of all uploads in the session, and when the session ends, Shiny deletes
|
||||
#' all of the subdirectories where files where uploaded to.
|
||||
#'
|
||||
#' @family input elements
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param multiple Whether the user should be allowed to select and upload
|
||||
#' multiple files at once. **Does not work on older browsers, including
|
||||
#' Internet Explorer 9 and earlier.**
|
||||
#' @param accept A character vector of "unique file type specifiers" which gives
|
||||
#' the browser a hint as to the type of file the server expects. Many browsers
|
||||
#' use this prevent the user from selecting an invalid file.
|
||||
#'
|
||||
#' A unique file type specifier can be:
|
||||
#' * A case insensitive extension like `.csv` or `.rds`.
|
||||
#' * A valid MIME type, like `text/plain` or `application/pdf`
|
||||
#' * One of `audio/*`, `video/*`, or `image/*` meaning any audio, video,
|
||||
#' or image type, respectively.
|
||||
#' @param buttonLabel The label used on the button. Can be text or an HTML tag
|
||||
#' object.
|
||||
#' @param placeholder The text to show before a file has been uploaded.
|
||||
#' @param capture What source to use for capturing image, audio or video data.
|
||||
#' This attribute facilitates user access to a device's media capture
|
||||
#' mechanism, such as a camera, or microphone, from within a file upload
|
||||
#' control.
|
||||
#'
|
||||
#' A value of `user` indicates that the user-facing camera and/or microphone
|
||||
#' should be used. A value of `environment` specifies that the outward-facing
|
||||
#' camera and/or microphone should be used.
|
||||
#'
|
||||
#' By default on most phones, this will accept still photos or video. For
|
||||
#' still photos only, also use `accept="image/*"`. For video only, use
|
||||
#' `accept="video/*"`.
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' fileInput("file1", "Choose CSV File", accept = ".csv"),
|
||||
#' checkboxInput("header", "Header", TRUE)
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' tableOutput("contents")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$contents <- renderTable({
|
||||
#' file <- input$file1
|
||||
#' ext <- tools::file_ext(file$datapath)
|
||||
#'
|
||||
#' req(file)
|
||||
#' validate(need(ext == "csv", "Please upload a csv file"))
|
||||
#'
|
||||
#' read.csv(file$datapath, header = input$header)
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#'
|
||||
#' A `data.frame` that contains one row for each selected file, and following
|
||||
#' columns:
|
||||
#' \describe{
|
||||
#' \item{`name`}{The filename provided by the web browser. This is
|
||||
#' **not** the path to read to get at the actual data that was uploaded
|
||||
#' (see
|
||||
#' `datapath` column).}
|
||||
#' \item{`size`}{The size of the uploaded data, in
|
||||
#' bytes.}
|
||||
#' \item{`type`}{The MIME type reported by the browser (for example,
|
||||
#' `text/plain`), or empty string if the browser didn't know.}
|
||||
#' \item{`datapath`}{The path to a temp file that contains the data that was
|
||||
#' uploaded. This file may be deleted if the user performs another upload
|
||||
#' operation.}
|
||||
#' }
|
||||
#'
|
||||
#' @export
|
||||
fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
|
||||
width = NULL, buttonLabel = "Browse...", placeholder = "No file selected",
|
||||
capture = NULL) {
|
||||
|
||||
restoredValue <- restoreInput(id = inputId, default = NULL)
|
||||
|
||||
# Catch potential edge case - ensure that it's either NULL or a data frame.
|
||||
if (!is.null(restoredValue) && !is.data.frame(restoredValue)) {
|
||||
warning("Restored value for ", inputId, " has incorrect format.")
|
||||
restoredValue <- NULL
|
||||
}
|
||||
|
||||
if (!is.null(restoredValue)) {
|
||||
restoredValue <- toJSON(restoredValue, strict_atomic = FALSE)
|
||||
}
|
||||
|
||||
inputTag <- tags$input(
|
||||
id = inputId,
|
||||
class = "shiny-input-file",
|
||||
name = inputId,
|
||||
type = "file",
|
||||
# Don't use "display: none;" style, which causes keyboard accessibility issue; instead use the following workaround: https://css-tricks.com/places-its-tempting-to-use-display-none-but-dont/
|
||||
style = "position: absolute !important; top: -99999px !important; left: -99999px !important;",
|
||||
`data-restore` = restoredValue
|
||||
)
|
||||
|
||||
if (multiple)
|
||||
inputTag$attribs$multiple <- "multiple"
|
||||
if (length(accept) > 0)
|
||||
inputTag$attribs$accept <- paste(accept, collapse=',')
|
||||
|
||||
if (!is.null(capture)) {
|
||||
inputTag$attribs$capture <- capture
|
||||
}
|
||||
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
|
||||
div(class = "input-group",
|
||||
# input-group-prepend is for bootstrap 4 compat
|
||||
tags$label(class = "input-group-btn input-group-prepend",
|
||||
span(class = "btn btn-default btn-file",
|
||||
buttonLabel,
|
||||
inputTag
|
||||
)
|
||||
),
|
||||
tags$input(type = "text", class = "form-control",
|
||||
placeholder = placeholder, readonly = "readonly"
|
||||
)
|
||||
),
|
||||
|
||||
tags$div(
|
||||
id=paste(inputId, "_progress", sep=""),
|
||||
class="progress active shiny-file-input-progress",
|
||||
tags$div(class="progress-bar")
|
||||
)
|
||||
)
|
||||
}
|
||||
66
R/input-numeric.R
Normal file
66
R/input-numeric.R
Normal file
@@ -0,0 +1,66 @@
|
||||
#' Create a numeric input control
|
||||
#'
|
||||
#' Create an input control for entry of numeric values
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param min Minimum allowed value
|
||||
#' @param max Maximum allowed value
|
||||
#' @param step Interval to use when stepping between min and max
|
||||
#' @return A numeric input control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [updateNumericInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' numericInput("obs", "Observations:", 10, min = 1, max = 100),
|
||||
#' verbatimTextOutput("value")
|
||||
#' )
|
||||
#' server <- function(input, output) {
|
||||
#' output$value <- renderText({ input$obs })
|
||||
#' }
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A numeric vector of length 1.
|
||||
#'
|
||||
#' @export
|
||||
numericInput <- function(
|
||||
inputId,
|
||||
label,
|
||||
value,
|
||||
min = NA,
|
||||
max = NA,
|
||||
step = NA,
|
||||
width = NULL,
|
||||
...,
|
||||
updateOn = c("change", "blur")
|
||||
) {
|
||||
rlang::check_dots_empty()
|
||||
updateOn <- rlang::arg_match(updateOn)
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
# build input tag
|
||||
inputTag <- tags$input(
|
||||
id = inputId,
|
||||
type = "number",
|
||||
class = "shiny-input-number form-control",
|
||||
value = formatNoSci(value),
|
||||
`data-update-on` = updateOn
|
||||
)
|
||||
if (!is.na(min)) inputTag$attribs$min = min
|
||||
if (!is.na(max)) inputTag$attribs$max = max
|
||||
if (!is.na(step)) inputTag$attribs$step = step
|
||||
|
||||
div(
|
||||
class = "form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
inputTag
|
||||
)
|
||||
}
|
||||
58
R/input-password.R
Normal file
58
R/input-password.R
Normal file
@@ -0,0 +1,58 @@
|
||||
#' Create a password input control
|
||||
#'
|
||||
#' Create an password control for entry of passwords.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @return A text input control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [updateTextInput()]
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A character string of the password input. The default value is `""`
|
||||
#' unless `value` is provided.
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' passwordInput("password", "Password:"),
|
||||
#' actionButton("go", "Go"),
|
||||
#' verbatimTextOutput("value")
|
||||
#' )
|
||||
#' server <- function(input, output) {
|
||||
#' output$value <- renderText({
|
||||
#' req(input$go)
|
||||
#' isolate(input$password)
|
||||
#' })
|
||||
#' }
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
passwordInput <- function(
|
||||
inputId,
|
||||
label,
|
||||
value = "",
|
||||
width = NULL,
|
||||
placeholder = NULL,
|
||||
...,
|
||||
updateOn = c("change", "blur")
|
||||
) {
|
||||
rlang::check_dots_empty()
|
||||
updateOn <- rlang::arg_match(updateOn)
|
||||
|
||||
div(
|
||||
class = "form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$input(
|
||||
id = inputId,
|
||||
type = "password",
|
||||
class = "shiny-input-password form-control",
|
||||
value = value,
|
||||
placeholder = placeholder,
|
||||
`data-update-on` = updateOn
|
||||
)
|
||||
)
|
||||
}
|
||||
117
R/input-radiobuttons.R
Normal file
117
R/input-radiobuttons.R
Normal file
@@ -0,0 +1,117 @@
|
||||
#' Create radio buttons
|
||||
#'
|
||||
#' Create a set of radio buttons used to select an item from a list.
|
||||
#'
|
||||
#' If you need to represent a "None selected" state, it's possible to default
|
||||
#' the radio buttons to have no options selected by using `selected =
|
||||
#' character(0)`. However, this is not recommended, as it gives the user no way
|
||||
#' to return to that state once they've made a selection. Instead, consider
|
||||
#' having the first of your choices be `c("None selected" = "")`.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param choices List of values to select from (if elements of the list are
|
||||
#' named then that name rather than the value is displayed to the user). If
|
||||
#' this argument is provided, then `choiceNames` and `choiceValues` must not
|
||||
#' be provided, and vice-versa. The values should be strings; other types
|
||||
#' (such as logicals and numbers) will be coerced to strings.
|
||||
#' @param selected The initially selected value. If not specified, then it
|
||||
#' defaults to the first item in `choices`. To start with no items selected,
|
||||
#' use `character(0)`.
|
||||
#' @param inline If `TRUE`, render the choices inline (i.e. horizontally)
|
||||
#' @return A set of radio buttons that can be added to a UI definition.
|
||||
#' @param choiceNames,choiceValues List of names and values, respectively, that
|
||||
#' are displayed to the user in the app and correspond to the each choice (for
|
||||
#' this reason, `choiceNames` and `choiceValues` must have the same length).
|
||||
#' If either of these arguments is provided, then the other *must* be provided
|
||||
#' and `choices` *must not* be provided. The advantage of using both of these
|
||||
#' over a named list for `choices` is that `choiceNames` allows any type of UI
|
||||
#' object to be passed through (tag objects, icons, HTML code, ...), instead
|
||||
#' of just simple text. See Examples.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [updateRadioButtons()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' radioButtons("dist", "Distribution type:",
|
||||
#' c("Normal" = "norm",
|
||||
#' "Uniform" = "unif",
|
||||
#' "Log-normal" = "lnorm",
|
||||
#' "Exponential" = "exp")),
|
||||
#' plotOutput("distPlot")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' dist <- switch(input$dist,
|
||||
#' norm = rnorm,
|
||||
#' unif = runif,
|
||||
#' lnorm = rlnorm,
|
||||
#' exp = rexp,
|
||||
#' rnorm)
|
||||
#'
|
||||
#' hist(dist(500))
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' radioButtons("rb", "Choose one:",
|
||||
#' choiceNames = list(
|
||||
#' icon("calendar"),
|
||||
#' HTML("<p style='color:red;'>Red Text</p>"),
|
||||
#' "Normal text"
|
||||
#' ),
|
||||
#' choiceValues = list(
|
||||
#' "icon", "html", "text"
|
||||
#' )),
|
||||
#' textOutput("txt")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$txt <- renderText({
|
||||
#' paste("You chose", input$rb)
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#'
|
||||
#' A character string containing the value of the selected button.
|
||||
#'
|
||||
#' @export
|
||||
radioButtons <- function(inputId, label, choices = NULL, selected = NULL,
|
||||
inline = FALSE, width = NULL, choiceNames = NULL, choiceValues = NULL) {
|
||||
|
||||
args <- normalizeChoicesArgs(choices, choiceNames, choiceValues)
|
||||
|
||||
selected <- restoreInput(id = inputId, default = selected)
|
||||
|
||||
# default value if it's not specified
|
||||
selected <- if (is.null(selected)) args$choiceValues[[1]] else as.character(selected)
|
||||
|
||||
if (length(selected) > 1) stop("The 'selected' argument must be of length 1")
|
||||
|
||||
options <- generateOptions(inputId, selected, inline,
|
||||
'radio', args$choiceNames, args$choiceValues)
|
||||
|
||||
divClass <- "form-group shiny-input-radiogroup shiny-input-container"
|
||||
if (inline) divClass <- paste(divClass, "shiny-input-container-inline")
|
||||
|
||||
inputLabel <- shinyInputLabel(inputId, label)
|
||||
tags$div(id = inputId,
|
||||
style = css(width = validateCssUnit(width)),
|
||||
class = divClass,
|
||||
# https://www.w3.org/TR/2017/WD-wai-aria-practices-1.1-20170628/examples/radio/radio-1/radio-1.html
|
||||
role = "radiogroup",
|
||||
`aria-labelledby` = inputLabel$attribs$id,
|
||||
inputLabel,
|
||||
options
|
||||
)
|
||||
}
|
||||
421
R/input-select.R
Normal file
421
R/input-select.R
Normal file
@@ -0,0 +1,421 @@
|
||||
#' Create a select list input control
|
||||
#'
|
||||
#' Create a select list that can be used to choose a single or multiple items
|
||||
#' from a list of values.
|
||||
#'
|
||||
#' By default, `selectInput()` and `selectizeInput()` use the JavaScript library
|
||||
#' \pkg{selectize.js} (<https://selectize.dev/>) instead of
|
||||
#' the basic select input element. To use the standard HTML select input
|
||||
#' element, use `selectInput()` with `selectize=FALSE`.
|
||||
#'
|
||||
#' In selectize mode, if the first element in `choices` has a value of `""`, its
|
||||
#' name will be treated as a placeholder prompt. For example:
|
||||
#' `selectInput("letter", "Letter", c("Choose one" = "", LETTERS))`
|
||||
#'
|
||||
#' **Performance note:** `selectInput()` and `selectizeInput()` can slow down
|
||||
#' significantly when thousands of choices are used; with legacy browsers like
|
||||
#' Internet Explorer, the user interface may hang for many seconds. For large
|
||||
#' numbers of choices, Shiny offers a "server-side selectize" option that
|
||||
#' massively improves performance and efficiency; see
|
||||
#' [this selectize article](https://shiny.rstudio.com/articles/selectize.html)
|
||||
#' on the Shiny Dev Center for details.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param choices List of values to select from. If elements of the list are
|
||||
#' named, then that name --- rather than the value --- is displayed to the
|
||||
#' user. It's also possible to group related inputs by providing a named list
|
||||
#' whose elements are (either named or unnamed) lists, vectors, or factors. In
|
||||
#' this case, the outermost names will be used as the group labels (leveraging
|
||||
#' the `<optgroup>` HTML tag) for the elements in the respective sublist. See
|
||||
#' the example section for a small demo of this feature.
|
||||
#' @param selected The initially selected value (or multiple values if `multiple
|
||||
#' = TRUE`). If not specified then defaults to the first value for
|
||||
#' single-select lists and no values for multiple select lists.
|
||||
#' @param multiple Is selection of multiple items allowed?
|
||||
#' @param selectize Whether to use \pkg{selectize.js} or not.
|
||||
#' @param size Number of items to show in the selection box; a larger number
|
||||
#' will result in a taller box. Not compatible with `selectize=TRUE`.
|
||||
#' Normally, when `multiple=FALSE`, a select input will be a drop-down list,
|
||||
#' but when `size` is set, it will be a box instead.
|
||||
#' @return A select list control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [updateSelectInput()] [varSelectInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # basic example
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' selectInput("variable", "Variable:",
|
||||
#' c("Cylinders" = "cyl",
|
||||
#' "Transmission" = "am",
|
||||
#' "Gears" = "gear")),
|
||||
#' tableOutput("data")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$data <- renderTable({
|
||||
#' mtcars[, c("mpg", input$variable), drop = FALSE]
|
||||
#' }, rownames = TRUE)
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' # demoing group support in the `choices` arg
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' selectInput("state", "Choose a state:",
|
||||
#' list(`East Coast` = list("NY", "NJ", "CT"),
|
||||
#' `West Coast` = list("WA", "OR", "CA"),
|
||||
#' `Midwest` = list("MN", "WI", "IA"))
|
||||
#' ),
|
||||
#' textOutput("result")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$result <- renderText({
|
||||
#' paste("You chose", input$state)
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value: A vector of character strings, usually of length
|
||||
#' 1, with the value of the selected items. When `multiple=TRUE` and
|
||||
#' nothing is selected, this value will be `NULL`.
|
||||
#'
|
||||
#' @export
|
||||
selectInput <- function(inputId, label, choices, selected = NULL,
|
||||
multiple = FALSE, selectize = TRUE, width = NULL,
|
||||
size = NULL) {
|
||||
|
||||
selected <- restoreInput(id = inputId, default = selected)
|
||||
|
||||
# resolve names
|
||||
choices <- choicesWithNames(choices)
|
||||
|
||||
# default value if it's not specified
|
||||
if (is.null(selected)) {
|
||||
if (!multiple) selected <- firstChoice(choices)
|
||||
} else selected <- as.character(selected)
|
||||
|
||||
if (!is.null(size) && selectize) {
|
||||
stop("'size' argument is incompatible with 'selectize=TRUE'.")
|
||||
}
|
||||
|
||||
# create select tag and add options
|
||||
selectTag <- tags$select(
|
||||
id = inputId,
|
||||
class = "shiny-input-select",
|
||||
class = if (!selectize) "form-control",
|
||||
size = size,
|
||||
selectOptions(choices, selected, inputId, selectize)
|
||||
)
|
||||
if (multiple)
|
||||
selectTag$attribs$multiple <- "multiple"
|
||||
|
||||
# return label and select tag
|
||||
res <- div(
|
||||
class = "form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
div(selectTag)
|
||||
)
|
||||
|
||||
if (!selectize) return(res)
|
||||
|
||||
selectizeIt(inputId, res, NULL, nonempty = !multiple && !("" %in% choices))
|
||||
}
|
||||
|
||||
firstChoice <- function(choices) {
|
||||
if (length(choices) == 0L) return()
|
||||
choice <- choices[[1]]
|
||||
if (is.list(choice)) firstChoice(choice) else choice
|
||||
}
|
||||
|
||||
# Create tags for each of the options; use <optgroup> if necessary.
|
||||
# This returns a HTML string instead of tags for performance reasons.
|
||||
selectOptions <- function(choices, selected = NULL, inputId, perfWarning = FALSE) {
|
||||
if (length(choices) >= 1000) {
|
||||
warning("The select input \"", inputId, "\" contains a large number of ",
|
||||
"options; consider using server-side selectize for massively improved ",
|
||||
"performance. See the Details section of the ?selectizeInput help topic.",
|
||||
call. = FALSE)
|
||||
}
|
||||
|
||||
html <- mapply(choices, names(choices), FUN = function(choice, label) {
|
||||
if (is.list(choice)) {
|
||||
# If sub-list, create an optgroup and recurse into the sublist
|
||||
sprintf(
|
||||
'<optgroup label="%s">\n%s\n</optgroup>',
|
||||
htmlEscape(label, TRUE),
|
||||
selectOptions(choice, selected, inputId, perfWarning)
|
||||
)
|
||||
|
||||
} else {
|
||||
# If single item, just return option string
|
||||
sprintf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
htmlEscape(choice, TRUE),
|
||||
if (choice %in% selected) ' selected' else '',
|
||||
htmlEscape(label)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
HTML(paste(html, collapse = '\n'))
|
||||
}
|
||||
|
||||
# need <optgroup> when choices contains sub-lists
|
||||
needOptgroup <- function(choices) {
|
||||
any(vapply(choices, is.list, logical(1)))
|
||||
}
|
||||
|
||||
#' @rdname selectInput
|
||||
#' @param ... Arguments passed to `selectInput()`.
|
||||
#' @param options A list of options. See the documentation of \pkg{selectize.js}(<https://selectize.dev/docs/usage>)
|
||||
#' for possible options (character option values inside [base::I()] will
|
||||
#' be treated as literal JavaScript code; see [renderDataTable()]
|
||||
#' for details).
|
||||
#' @param width The width of the input, e.g. `'400px'`, or `'100%'`;
|
||||
#' see [validateCssUnit()].
|
||||
#' @note The selectize input created from `selectizeInput()` allows
|
||||
#' deletion of the selected option even in a single select input, which will
|
||||
#' return an empty string as its value. This is the default behavior of
|
||||
#' \pkg{selectize.js}. However, the selectize input created from
|
||||
#' `selectInput(..., selectize = TRUE)` will ignore the empty string
|
||||
#' value when it is a single choice input and the empty string is not in the
|
||||
#' `choices` argument. This is to keep compatibility with
|
||||
#' `selectInput(..., selectize = FALSE)`.
|
||||
#' @export
|
||||
selectizeInput <- function(inputId, ..., options = NULL, width = NULL) {
|
||||
selectizeIt(
|
||||
inputId,
|
||||
selectInput(inputId, ..., selectize = FALSE, width = width),
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
# given a select input and its id, selectize it
|
||||
selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
|
||||
if (length(options) == 0) {
|
||||
# For NULL and empty unnamed list, replace with an empty named list, so that
|
||||
# it will get translated to {} in JSON later on.
|
||||
options <- empty_named_list()
|
||||
}
|
||||
|
||||
# Make sure accessibility plugin is included
|
||||
if (!('selectize-plugin-a11y' %in% options$plugins)) {
|
||||
options$plugins <- c(options$plugins, list('selectize-plugin-a11y'))
|
||||
}
|
||||
|
||||
res <- checkAsIs(options)
|
||||
|
||||
deps <- list(selectizeDependency())
|
||||
|
||||
if ('drag_drop' %in% options$plugins) {
|
||||
deps[[length(deps) + 1]] <- jqueryuiDependency()
|
||||
}
|
||||
|
||||
# Insert script on same level as <select> tag
|
||||
select$children[[2]] <- tagAppendChild(
|
||||
select$children[[2]],
|
||||
tags$script(
|
||||
type = 'application/json',
|
||||
`data-for` = inputId, `data-nonempty` = if (nonempty) '',
|
||||
`data-eval` = if (length(res$eval)) HTML(toJSON(res$eval)),
|
||||
HTML(toJSON(res$options))
|
||||
)
|
||||
)
|
||||
|
||||
attachDependencies(select, deps)
|
||||
}
|
||||
|
||||
|
||||
selectizeDependency <- function() {
|
||||
bslib::bs_dependency_defer(selectizeDependencyFunc)
|
||||
}
|
||||
|
||||
selectizeDependencyFunc <- function(theme) {
|
||||
if (!is_bs_theme(theme)) {
|
||||
return(selectizeStaticDependency(version_selectize))
|
||||
}
|
||||
|
||||
bs_version <- bslib::theme_version(theme)
|
||||
|
||||
# It'd be cleaner to ship the JS in a separate, href-based,
|
||||
# HTML dependency (which we currently do for other themable widgets),
|
||||
# but DT, crosstalk, and maybe other pkgs include selectize JS/CSS
|
||||
# in HTML dependency named selectize, so if we were to change that
|
||||
# name, the JS/CSS would be loaded/included twice, which leads to
|
||||
# strange issues, especially since we now include a 3rd party
|
||||
# accessibility plugin https://github.com/rstudio/shiny/pull/3153
|
||||
selectizeDir <- system_file(package = "shiny", "www/shared/selectize/")
|
||||
script <- file.path(selectizeDir, selectizeScripts())
|
||||
|
||||
bslib::bs_dependency(
|
||||
input = selectizeSass(bs_version),
|
||||
theme = theme,
|
||||
name = "selectize",
|
||||
version = version_selectize,
|
||||
cache_key_extra = get_package_version("shiny"),
|
||||
.dep_args = list(script = script)
|
||||
)
|
||||
}
|
||||
|
||||
selectizeSass <- function(bs_version) {
|
||||
selectizeDir <- system_file(package = "shiny", "www/shared/selectize/")
|
||||
stylesheet <- file.path(
|
||||
selectizeDir, "scss", paste0("selectize.bootstrap", bs_version, ".scss")
|
||||
)
|
||||
sass::sass_file(stylesheet)
|
||||
}
|
||||
|
||||
selectizeStaticDependency <- function(version) {
|
||||
htmlDependency(
|
||||
"selectize",
|
||||
version,
|
||||
src = "www/shared/selectize",
|
||||
package = "shiny",
|
||||
stylesheet = "css/selectize.bootstrap3.css",
|
||||
script = selectizeScripts()
|
||||
)
|
||||
}
|
||||
|
||||
selectizeScripts <- function() {
|
||||
isMinified <- isTRUE(get_devmode_option("shiny.minified", TRUE))
|
||||
paste0(
|
||||
c(
|
||||
"js/selectize",
|
||||
"accessibility/js/selectize-plugin-a11y"
|
||||
),
|
||||
if (isMinified) ".min.js" else ".js"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' Select variables from a data frame
|
||||
#'
|
||||
#' Create a select list that can be used to choose a single or multiple items
|
||||
#' from the column names of a data frame.
|
||||
#'
|
||||
#' By default, `varSelectInput()` and `selectizeInput()` use the
|
||||
#' JavaScript library \pkg{selectize.js}
|
||||
#' (<https://selectize.dev/>) to instead of the basic
|
||||
#' select input element. To use the standard HTML select input element, use
|
||||
#' `selectInput()` with `selectize=FALSE`.
|
||||
#'
|
||||
#' @inheritParams selectInput
|
||||
#' @param data A data frame. Used to retrieve the column names as choices for a [selectInput()]
|
||||
#' @return A variable select list control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [updateSelectInput()]
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' The resulting server `input` value will be returned as:
|
||||
#'
|
||||
#' * A symbol if `multiple = FALSE`. The `input` value should be
|
||||
#' used with rlang's [rlang::!!()]. For example,
|
||||
#' `ggplot2::aes(!!input$variable)`.
|
||||
#' * A list of symbols if `multiple = TRUE`. The `input` value
|
||||
#' should be used with rlang's [rlang::!!!()] to expand
|
||||
#' the symbol list as individual arguments. For example,
|
||||
#' `dplyr::select(mtcars, !!!input$variabls)` which is
|
||||
#' equivalent to `dplyr::select(mtcars, !!input$variabls[[1]], !!input$variabls[[2]], ..., !!input$variabls[[length(input$variabls)]])`.
|
||||
#'
|
||||
#' @examples
|
||||
#'
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' library(ggplot2)
|
||||
#'
|
||||
#' # single selection
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' varSelectInput("variable", "Variable:", mtcars),
|
||||
#' plotOutput("data")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$data <- renderPlot({
|
||||
#' ggplot(mtcars, aes(!!input$variable)) + geom_histogram()
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # multiple selections
|
||||
#' \dontrun{
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' varSelectInput("variables", "Variable:", mtcars, multiple = TRUE),
|
||||
#' tableOutput("data")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$data <- renderTable({
|
||||
#' if (length(input$variables) == 0) return(mtcars)
|
||||
#' mtcars %>% dplyr::select(!!!input$variables)
|
||||
#' }, rownames = TRUE)
|
||||
#' }
|
||||
#' )}
|
||||
#'
|
||||
#' }
|
||||
#' @export
|
||||
varSelectInput <- function(
|
||||
inputId, label, data, selected = NULL,
|
||||
multiple = FALSE, selectize = TRUE, width = NULL,
|
||||
size = NULL
|
||||
) {
|
||||
# no place holders
|
||||
choices <- colnames(data)
|
||||
|
||||
selectInputVal <- selectInput(
|
||||
inputId = inputId,
|
||||
label = label,
|
||||
choices = choices,
|
||||
selected = selected,
|
||||
multiple = multiple,
|
||||
selectize = selectize,
|
||||
width = width,
|
||||
size = size
|
||||
)
|
||||
|
||||
# set the select tag class to be "symbol"
|
||||
selectClass <- selectInputVal$children[[2]]$children[[1]]$attribs$class
|
||||
if (is.null(selectClass)) {
|
||||
newClass <- "symbol"
|
||||
} else {
|
||||
newClass <- paste(selectClass, "symbol", sep = " ")
|
||||
}
|
||||
selectInputVal$children[[2]]$children[[1]]$attribs$class <- newClass
|
||||
|
||||
selectInputVal
|
||||
}
|
||||
|
||||
|
||||
|
||||
#' @rdname varSelectInput
|
||||
#' @param ... Arguments passed to `varSelectInput()`.
|
||||
#' @param options A list of options. See the documentation of \pkg{selectize.js}(<https://selectize.dev/docs/usage>)
|
||||
#' for possible options (character option values inside [base::I()] will
|
||||
#' be treated as literal JavaScript code; see [renderDataTable()]
|
||||
#' for details).
|
||||
#' @param width The width of the input, e.g. `'400px'`, or `'100%'`;
|
||||
#' see [validateCssUnit()].
|
||||
#' @note The variable selectize input created from `varSelectizeInput()` allows
|
||||
#' deletion of the selected option even in a single select input, which will
|
||||
#' return an empty string as its value. This is the default behavior of
|
||||
#' \pkg{selectize.js}. However, the selectize input created from
|
||||
#' `selectInput(..., selectize = TRUE)` will ignore the empty string
|
||||
#' value when it is a single choice input and the empty string is not in the
|
||||
#' `choices` argument. This is to keep compatibility with
|
||||
#' `selectInput(..., selectize = FALSE)`.
|
||||
#' @export
|
||||
varSelectizeInput <- function(inputId, ..., options = NULL, width = NULL) {
|
||||
selectizeIt(
|
||||
inputId,
|
||||
varSelectInput(inputId, ..., selectize = FALSE, width = width),
|
||||
options
|
||||
)
|
||||
}
|
||||
336
R/input-slider.R
Normal file
336
R/input-slider.R
Normal file
@@ -0,0 +1,336 @@
|
||||
#' Slider Input Widget
|
||||
#'
|
||||
#' Constructs a slider widget to select a number, date, or date-time from a
|
||||
#' range.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param min,max The minimum and maximum values (inclusive) that can be
|
||||
#' selected.
|
||||
#' @param value The initial value of the slider, either a number, a date
|
||||
#' (class Date), or a date-time (class POSIXt). A length one vector will
|
||||
#' create a regular slider; a length two vector will create a double-ended
|
||||
#' range slider. Must lie between `min` and `max`.
|
||||
#' @param step Specifies the interval between each selectable value on the
|
||||
#' slider. Either `NULL`, the default, which uses a heuristic to determine the
|
||||
#' step size or a single number. If the values are dates, `step` is in days;
|
||||
#' if the values are date-times, `step` is in seconds.
|
||||
#' @param round `TRUE` to round all values to the nearest integer;
|
||||
#' `FALSE` if no rounding is desired; or an integer to round to that
|
||||
#' number of digits (for example, 1 will round to the nearest 10, and -2 will
|
||||
#' round to the nearest .01). Any rounding will be applied after snapping to
|
||||
#' the nearest step.
|
||||
#' @param ticks `FALSE` to hide tick marks, `TRUE` to show them
|
||||
#' according to some simple heuristics.
|
||||
#' @param animate `TRUE` to show simple animation controls with default
|
||||
#' settings; `FALSE` not to; or a custom settings list, such as those
|
||||
#' created using [animationOptions()].
|
||||
#' @param sep Separator between thousands places in numbers.
|
||||
#' @param pre A prefix string to put in front of the value.
|
||||
#' @param post A suffix string to put after the value.
|
||||
#' @param dragRange This option is used only if it is a range slider (with two
|
||||
#' values). If `TRUE` (the default), the range can be dragged. In other
|
||||
#' words, the min and max can be dragged together. If `FALSE`, the range
|
||||
#' cannot be dragged.
|
||||
#' @param timeFormat Only used if the values are Date or POSIXt objects. A time
|
||||
#' format string, to be passed to the Javascript strftime library. See
|
||||
#' <https://github.com/samsonjs/strftime> for more details. The allowed
|
||||
#' format specifications are very similar, but not identical, to those for R's
|
||||
#' [base::strftime()] function. For Dates, the default is `"%F"`
|
||||
#' (like `"2015-07-01"`), and for POSIXt, the default is `"%F %T"`
|
||||
#' (like `"2015-07-01 15:32:10"`).
|
||||
#' @param timezone Only used if the values are POSIXt objects. A string
|
||||
#' specifying the time zone offset for the displayed times, in the format
|
||||
#' `"+HHMM"` or `"-HHMM"`. If `NULL` (the default), times will
|
||||
#' be displayed in the browser's time zone. The value `"+0000"` will
|
||||
#' result in UTC time.
|
||||
#' @inheritParams selectizeInput
|
||||
#' @family input elements
|
||||
#' @seealso [updateSliderInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sliderInput("obs", "Number of observations:",
|
||||
#' min = 0, max = 1000, value = 500
|
||||
#' ),
|
||||
#' plotOutput("distPlot")
|
||||
#' )
|
||||
#'
|
||||
#' # Server logic
|
||||
#' server <- function(input, output) {
|
||||
#' output$distPlot <- renderPlot({
|
||||
#' hist(rnorm(input$obs))
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' # Complete app with UI and server components
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A number, date, or date-time (depending on the class of `value`), or
|
||||
#' in the case of slider range, a vector of two numbers/dates/date-times.
|
||||
#'
|
||||
#' @export
|
||||
sliderInput <- function(inputId, label, min, max, value, step = NULL,
|
||||
round = FALSE, ticks = TRUE, animate = FALSE,
|
||||
width = NULL, sep = ",", pre = NULL, post = NULL,
|
||||
timeFormat = NULL, timezone = NULL, dragRange = TRUE) {
|
||||
validate_slider_value(min, max, value, "sliderInput")
|
||||
|
||||
dataType <- getSliderType(min, max, value)
|
||||
|
||||
if (is.null(timeFormat)) {
|
||||
timeFormat <- switch(dataType, date = "%F", datetime = "%F %T", number = NULL)
|
||||
}
|
||||
|
||||
# Restore bookmarked values here, after doing the type checking, because the
|
||||
# restored value will be a character vector instead of Date or POSIXct, and we can do
|
||||
# the conversion to correct type next.
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
if (is.character(value)) {
|
||||
# If we got here, the value was restored from a URL-encoded bookmark.
|
||||
if (dataType == "date") {
|
||||
value <- as.Date(value, format = "%Y-%m-%d")
|
||||
} else if (dataType == "datetime") {
|
||||
# Date-times will have a format like "2018-02-28T03:46:26Z"
|
||||
value <- as.POSIXct(value, format = "%Y-%m-%dT%H:%M:%SZ", tz = "UTC")
|
||||
}
|
||||
}
|
||||
|
||||
step <- findStepSize(min, max, step)
|
||||
|
||||
if (dataType %in% c("date", "datetime")) {
|
||||
# For Dates, this conversion uses midnight on that date in UTC
|
||||
to_ms <- function(x) 1000 * as.numeric(as.POSIXct(x))
|
||||
|
||||
# Convert values to milliseconds since epoch (this is the value JS uses)
|
||||
# Find step size in ms
|
||||
step <- to_ms(max) - to_ms(max - step)
|
||||
min <- to_ms(min)
|
||||
max <- to_ms(max)
|
||||
value <- to_ms(value)
|
||||
}
|
||||
|
||||
range <- max - min
|
||||
|
||||
# Try to get a sane number of tick marks
|
||||
if (ticks) {
|
||||
n_steps <- range / step
|
||||
|
||||
# Make sure there are <= 10 steps.
|
||||
# n_ticks can be a noninteger, which is good when the range is not an
|
||||
# integer multiple of the step size, e.g., min=1, max=10, step=4
|
||||
scale_factor <- ceiling(n_steps / 10)
|
||||
n_ticks <- n_steps / scale_factor
|
||||
|
||||
} else {
|
||||
n_ticks <- NULL
|
||||
}
|
||||
|
||||
sliderProps <- dropNulls(list(
|
||||
class = "js-range-slider",
|
||||
id = inputId,
|
||||
`data-skin` = "shiny",
|
||||
`data-type` = if (length(value) > 1) "double",
|
||||
`data-min` = formatNoSci(min),
|
||||
`data-max` = formatNoSci(max),
|
||||
`data-from` = formatNoSci(value[1]),
|
||||
`data-to` = if (length(value) > 1) formatNoSci(value[2]),
|
||||
`data-step` = formatNoSci(step),
|
||||
`data-grid` = ticks,
|
||||
`data-grid-num` = n_ticks,
|
||||
`data-grid-snap` = FALSE,
|
||||
`data-prettify-separator` = sep,
|
||||
`data-prettify-enabled` = (sep != ""),
|
||||
`data-prefix` = pre,
|
||||
`data-postfix` = post,
|
||||
`data-keyboard` = TRUE,
|
||||
# This value is only relevant for range sliders; for non-range sliders it
|
||||
# causes problems since ion.RangeSlider 2.1.2 (issue #1605).
|
||||
`data-drag-interval` = if (length(value) > 1) dragRange,
|
||||
# The following are ignored by the ion.rangeSlider, but are used by Shiny.
|
||||
`data-data-type` = dataType,
|
||||
`data-time-format` = timeFormat,
|
||||
`data-timezone` = timezone
|
||||
))
|
||||
|
||||
# Replace any TRUE and FALSE with "true" and "false"
|
||||
sliderProps <- lapply(sliderProps, function(x) {
|
||||
if (identical(x, TRUE)) "true"
|
||||
else if (identical(x, FALSE)) "false"
|
||||
else x
|
||||
})
|
||||
|
||||
sliderTag <- div(class = "form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
do.call(tags$input, sliderProps)
|
||||
)
|
||||
|
||||
# Add animation buttons
|
||||
if (identical(animate, TRUE))
|
||||
animate <- animationOptions()
|
||||
|
||||
if (!is.null(animate) && !identical(animate, FALSE)) {
|
||||
if (is.null(animate$playButton))
|
||||
animate$playButton <- icon('play', lib = 'glyphicon')
|
||||
if (is.null(animate$pauseButton))
|
||||
animate$pauseButton <- icon('pause', lib = 'glyphicon')
|
||||
|
||||
sliderTag <- tagAppendChild(
|
||||
sliderTag,
|
||||
tags$div(class='slider-animate-container',
|
||||
tags$a(href='#',
|
||||
class='slider-animate-button',
|
||||
'data-target-id'=inputId,
|
||||
'data-interval'=animate$interval,
|
||||
'data-loop'=animate$loop,
|
||||
span(class = 'play', animate$playButton),
|
||||
span(class = 'pause', animate$pauseButton)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
attachDependencies(sliderTag, ionRangeSliderDependency())
|
||||
}
|
||||
|
||||
|
||||
ionRangeSliderDependency <- function() {
|
||||
list(
|
||||
# ion.rangeSlider also needs normalize.css, which is already included in Bootstrap.
|
||||
htmlDependency(
|
||||
"ionrangeslider-javascript",
|
||||
version_ion_range_slider,
|
||||
src = "www/shared/ionrangeslider",
|
||||
package = "shiny",
|
||||
script = "js/ion.rangeSlider.min.js"
|
||||
),
|
||||
htmlDependency(
|
||||
"strftime",
|
||||
version_strftime,
|
||||
src = "www/shared/strftime",
|
||||
package = "shiny",
|
||||
script = "strftime-min.js"
|
||||
),
|
||||
bslib::bs_dependency_defer(ionRangeSliderDependencyCSS)
|
||||
)
|
||||
}
|
||||
|
||||
ionRangeSliderDependencySass <- function() {
|
||||
list(
|
||||
list(accent = "$component-active-bg"),
|
||||
sass::sass_file(
|
||||
system_file(package = "shiny", "www/shared/ionrangeslider/scss/shiny.scss")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ionRangeSliderDependencyCSS <- function(theme) {
|
||||
if (!is_bs_theme(theme)) {
|
||||
return(htmlDependency(
|
||||
"ionrangeslider-css",
|
||||
version_ion_range_slider,
|
||||
src = "www/shared/ionrangeslider",
|
||||
package = "shiny",
|
||||
stylesheet = "css/ion.rangeSlider.css"
|
||||
))
|
||||
}
|
||||
|
||||
bslib::bs_dependency(
|
||||
input = ionRangeSliderDependencySass(),
|
||||
theme = theme,
|
||||
name = "ionRangeSlider",
|
||||
version = version_ion_range_slider,
|
||||
cache_key_extra = get_package_version("shiny")
|
||||
)
|
||||
}
|
||||
|
||||
hasDecimals <- function(value) {
|
||||
truncatedValue <- round(value)
|
||||
return (!identical(value, truncatedValue))
|
||||
}
|
||||
|
||||
# If step is NULL, use heuristic to set the step size.
|
||||
findStepSize <- function(min, max, step) {
|
||||
if (!is.null(step)) return(step)
|
||||
|
||||
range <- max - min
|
||||
# If short range or decimals, use continuous decimal with ~100 points
|
||||
if (range < 2 || hasDecimals(min) || hasDecimals(max)) {
|
||||
# Workaround for rounding errors (#1006): the intervals between the items
|
||||
# returned by pretty() can have rounding errors. To avoid this, we'll use
|
||||
# pretty() to find the min, max, and number of steps, and then use those
|
||||
# values to calculate the step size.
|
||||
pretty_steps <- pretty(c(min, max), n = 100)
|
||||
n_steps <- length(pretty_steps) - 1
|
||||
|
||||
# Fix for #2061: Windows has low-significance digits (like 17 digits out)
|
||||
# even at the boundaries of pretty()'s output. Use signif(digits = 10),
|
||||
# which should be way way less significant than any data we'd want to keep.
|
||||
# It might make sense to use signif(steps[2] - steps[1], 10) instead, but
|
||||
# for now trying to make the minimal change.
|
||||
signif(digits = 10, (max(pretty_steps) - min(pretty_steps)) / n_steps)
|
||||
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
# Throw a warning if ever `value` is not in the [`min`, `max`] range
|
||||
validate_slider_value <- function(min, max, value, fun) {
|
||||
if (length(min) != 1 || is_na(min) ||
|
||||
length(max) != 1 || is_na(max) ||
|
||||
length(value) < 1 || length(value) > 2 || any(is.na(value)))
|
||||
{
|
||||
stop(call. = FALSE,
|
||||
sprintf("In %s(): `min`, `max`, and `value` cannot be NULL, NA, or empty.", fun)
|
||||
)
|
||||
}
|
||||
|
||||
if (min(value) < min) {
|
||||
warning(call. = FALSE,
|
||||
sprintf(
|
||||
"In %s(): `value` should be greater than or equal to `min` (value = %s, min = %s).",
|
||||
fun, paste(value, collapse = ", "), min
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (max(value) > max) {
|
||||
warning(
|
||||
noBreaks. = TRUE, call. = FALSE,
|
||||
sprintf(
|
||||
"In %s(): `value` should be less than or equal to `max` (value = %s, max = %s).",
|
||||
fun, paste(value, collapse = ", "), max
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#' @rdname sliderInput
|
||||
#'
|
||||
#' @param interval The interval, in milliseconds, between each animation step.
|
||||
#' @param loop `TRUE` to automatically restart the animation when it
|
||||
#' reaches the end.
|
||||
#' @param playButton Specifies the appearance of the play button. Valid values
|
||||
#' are a one-element character vector (for a simple text label), an HTML tag
|
||||
#' or list of tags (using [tag()] and friends), or raw HTML (using
|
||||
#' [HTML()]).
|
||||
#' @param pauseButton Similar to `playButton`, but for the pause button.
|
||||
#' @export
|
||||
animationOptions <- function(interval=1000,
|
||||
loop=FALSE,
|
||||
playButton=NULL,
|
||||
pauseButton=NULL) {
|
||||
list(interval=interval,
|
||||
loop=loop,
|
||||
playButton=playButton,
|
||||
pauseButton=pauseButton)
|
||||
}
|
||||
65
R/input-submit.R
Normal file
65
R/input-submit.R
Normal file
@@ -0,0 +1,65 @@
|
||||
#' Create a submit button
|
||||
#'
|
||||
#' Create a submit button for an app. Apps that include a submit
|
||||
#' button do not automatically update their outputs when inputs change,
|
||||
#' rather they wait until the user explicitly clicks the submit button.
|
||||
#' The use of `submitButton` is generally discouraged in favor of
|
||||
#' the more versatile [actionButton()] (see details below).
|
||||
#'
|
||||
#' Submit buttons are unusual Shiny inputs, and we recommend using
|
||||
#' [actionButton()] instead of `submitButton` when you
|
||||
#' want to delay a reaction.
|
||||
#' See [this
|
||||
#' article](https://shiny.rstudio.com/articles/action-buttons.html) for more information (including a demo of how to "translate"
|
||||
#' code using a `submitButton` to code using an `actionButton`).
|
||||
#'
|
||||
#' In essence, the presence of a submit button stops all inputs from
|
||||
#' sending their values automatically to the server. This means, for
|
||||
#' instance, that if there are *two* submit buttons in the same app,
|
||||
#' clicking either one will cause all inputs in the app to send their
|
||||
#' values to the server. This is probably not what you'd want, which is
|
||||
#' why submit button are unwieldy for all but the simplest apps. There
|
||||
#' are other problems with submit buttons: for example, dynamically
|
||||
#' created submit buttons (for example, with [renderUI()]
|
||||
#' or [insertUI()]) will not work.
|
||||
#'
|
||||
#' @param text Button caption
|
||||
#' @param icon Optional [icon()] to appear on the button
|
||||
#' @param width The width of the button, e.g. `'400px'`, or `'100%'`;
|
||||
#' see [validateCssUnit()].
|
||||
#' @return A submit button that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#'
|
||||
#' @examples
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' shinyApp(
|
||||
#' ui = basicPage(
|
||||
#' numericInput("num", label = "Make changes", value = 1),
|
||||
#' submitButton("Update View", icon("refresh")),
|
||||
#' helpText("When you click the button above, you should see",
|
||||
#' "the output below update to reflect the value you",
|
||||
#' "entered at the top:"),
|
||||
#' verbatimTextOutput("value")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#'
|
||||
#' # submit buttons do not have a value of their own,
|
||||
#' # they control when the app accesses values of other widgets.
|
||||
#' # input$num is the value of the number widget.
|
||||
#' output$value <- renderPrint({ input$num })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#' @export
|
||||
submitButton <- function(text = "Apply Changes", icon = NULL, width = NULL) {
|
||||
div(
|
||||
tags$button(
|
||||
type="submit",
|
||||
class="btn btn-primary shiny-submit-button",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
list(icon, text)
|
||||
)
|
||||
)
|
||||
}
|
||||
72
R/input-text.R
Normal file
72
R/input-text.R
Normal file
@@ -0,0 +1,72 @@
|
||||
#' Create a text input control
|
||||
#'
|
||||
#' Create an input control for entry of unstructured text values
|
||||
#'
|
||||
#' @param inputId The `input` slot that will be used to access the value.
|
||||
#' @param label Display label for the control, or `NULL` for no label.
|
||||
#' @param value Initial value.
|
||||
#' @param width The width of the input, e.g. `'400px'`, or `'100%'`;
|
||||
#' see [validateCssUnit()].
|
||||
#' @param placeholder A character string giving the user a hint as to what can
|
||||
#' be entered into the control. Internet Explorer 8 and 9 do not support this
|
||||
#' option.
|
||||
#' @param ... Ignored, included to require named arguments and for future
|
||||
#' feature expansion.
|
||||
#' @param updateOn A character vector specifying when the input should be
|
||||
#' updated. Options are `"change"` (default) and `"blur"`. Use `"change"` to
|
||||
#' update the input immediately whenever the value changes. Use `"blur"`to
|
||||
#' delay the input update until the input loses focus (the user moves away
|
||||
#' from the input), or when Enter is pressed (or Cmd/Ctrl + Enter for
|
||||
#' [textAreaInput()]).
|
||||
#' @return A text input control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [updateTextInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' textInput("caption", "Caption", "Data Summary"),
|
||||
#' verbatimTextOutput("value")
|
||||
#' )
|
||||
#' server <- function(input, output) {
|
||||
#' output$value <- renderText({ input$caption })
|
||||
#' }
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A character string of the text input. The default value is `""`
|
||||
#' unless `value` is provided.
|
||||
#'
|
||||
#' @export
|
||||
textInput <- function(
|
||||
inputId,
|
||||
label,
|
||||
value = "",
|
||||
width = NULL,
|
||||
placeholder = NULL,
|
||||
...,
|
||||
updateOn = c("change", "blur")
|
||||
) {
|
||||
rlang::check_dots_empty()
|
||||
updateOn <- rlang::arg_match(updateOn)
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
div(
|
||||
class = "form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$input(
|
||||
id = inputId,
|
||||
type = "text",
|
||||
class = "shiny-input-text form-control",
|
||||
value = value,
|
||||
placeholder = placeholder,
|
||||
`data-update-on` = updateOn
|
||||
)
|
||||
)
|
||||
}
|
||||
96
R/input-textarea.R
Normal file
96
R/input-textarea.R
Normal file
@@ -0,0 +1,96 @@
|
||||
#' Create a textarea input control
|
||||
#'
|
||||
#' Create a textarea input control for entry of unstructured text values.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param height The height of the input, e.g. `'400px'`, or `'100%'`; see
|
||||
#' [validateCssUnit()].
|
||||
#' @param cols Value of the visible character columns of the input, e.g. `80`.
|
||||
#' This argument will only take effect if there is not a CSS `width` rule
|
||||
#' defined for this element; such a rule could come from the `width` argument
|
||||
#' of this function or from a containing page layout such as
|
||||
#' [fluidPage()].
|
||||
#' @param rows The value of the visible character rows of the input, e.g. `6`.
|
||||
#' If the `height` argument is specified, `height` will take precedence in the
|
||||
#' browser's rendering.
|
||||
#' @param resize Which directions the textarea box can be resized. Can be one of
|
||||
#' `"both"`, `"none"`, `"vertical"`, and `"horizontal"`. The default, `NULL`,
|
||||
#' will use the client browser's default setting for resizing textareas.
|
||||
#' @param autoresize If `TRUE`, the textarea will automatically resize to fit
|
||||
#' the input text.
|
||||
#' @return A textarea input control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [updateTextAreaInput()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' textAreaInput("caption", "Caption", "Data Summary", width = "1000px"),
|
||||
#' verbatimTextOutput("value")
|
||||
#' )
|
||||
#' server <- function(input, output) {
|
||||
#' output$value <- renderText({ input$caption })
|
||||
#' }
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A character string of the text input. The default value is `""`
|
||||
#' unless `value` is provided.
|
||||
#'
|
||||
#' @export
|
||||
textAreaInput <- function(
|
||||
inputId,
|
||||
label,
|
||||
value = "",
|
||||
width = NULL,
|
||||
height = NULL,
|
||||
cols = NULL,
|
||||
rows = NULL,
|
||||
placeholder = NULL,
|
||||
resize = NULL,
|
||||
...,
|
||||
autoresize = FALSE,
|
||||
updateOn = c("change", "blur")
|
||||
) {
|
||||
rlang::check_dots_empty()
|
||||
updateOn <- rlang::arg_match(updateOn)
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
if (!is.null(resize)) {
|
||||
resize <- match.arg(resize, c("both", "none", "vertical", "horizontal"))
|
||||
}
|
||||
|
||||
classes <- "form-control"
|
||||
if (autoresize) {
|
||||
classes <- c(classes, "textarea-autoresize")
|
||||
if (is.null(rows)) {
|
||||
rows <- 1
|
||||
}
|
||||
}
|
||||
|
||||
div(
|
||||
class = "shiny-input-textarea form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$textarea(
|
||||
id = inputId,
|
||||
class = classes,
|
||||
placeholder = placeholder,
|
||||
style = css(
|
||||
width = if (!is.null(width)) "100%",
|
||||
height = validateCssUnit(height),
|
||||
resize = resize
|
||||
),
|
||||
rows = rows,
|
||||
cols = cols,
|
||||
`data-update-on` = updateOn,
|
||||
value
|
||||
)
|
||||
)
|
||||
}
|
||||
174
R/input-utils.R
Normal file
174
R/input-utils.R
Normal file
@@ -0,0 +1,174 @@
|
||||
shinyInputLabel <- function(inputId, label = NULL) {
|
||||
tags$label(
|
||||
label,
|
||||
class = "control-label",
|
||||
class = if (is.null(label)) "shiny-label-null",
|
||||
# `id` attribute is required for `aria-labelledby` used by screen readers:
|
||||
id = paste0(inputId, "-label"),
|
||||
`for` = inputId
|
||||
)
|
||||
}
|
||||
|
||||
# This function takes in either a list or vector for `choices` (and
|
||||
# `choiceNames` and `choiceValues` are passed in as NULL) OR it takes
|
||||
# in a list or vector for both `choiceNames` and `choiceValues` (and
|
||||
# `choices` is passed as NULL) and returns a list of two elements:
|
||||
# - `choiceNames` is a vector or list that holds the options names
|
||||
# (each element can be arbitrary UI, or simple text)
|
||||
# - `choiceValues` is a vector or list that holds the options values
|
||||
# (each element must be simple text)
|
||||
normalizeChoicesArgs <- function(choices, choiceNames, choiceValues,
|
||||
mustExist = TRUE) {
|
||||
# if-else to check that either choices OR (choiceNames + choiceValues)
|
||||
# were correctly provided
|
||||
if (is.null(choices)) {
|
||||
if (is.null(choiceNames) || is.null(choiceValues)) {
|
||||
if (mustExist) {
|
||||
stop("Please specify a non-empty vector for `choices` (or, ",
|
||||
"alternatively, for both `choiceNames` AND `choiceValues`).")
|
||||
} else {
|
||||
if (is.null(choiceNames) && is.null(choiceValues)) {
|
||||
# this is useful when we call this function from `updateInputOptions()`
|
||||
# in which case, all three `choices`, `choiceNames` and `choiceValues`
|
||||
# may legitimately be NULL
|
||||
return(list(choiceNames = NULL, choiceValues = NULL))
|
||||
} else {
|
||||
stop("One of `choiceNames` or `choiceValues` was set to ",
|
||||
"NULL, but either both or none should be NULL.")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (length(choiceNames) != length(choiceValues)) {
|
||||
stop("`choiceNames` and `choiceValues` must have the same length.")
|
||||
}
|
||||
if (any_named(choiceNames) || any_named(choiceValues)) {
|
||||
stop("`choiceNames` and `choiceValues` must not be named.")
|
||||
}
|
||||
} else {
|
||||
if (!is.null(choiceNames) || !is.null(choiceValues)) {
|
||||
warning("Using `choices` argument; ignoring `choiceNames` and `choiceValues`.")
|
||||
}
|
||||
choices <- choicesWithNames(choices) # resolve names if not specified
|
||||
choiceNames <- names(choices)
|
||||
choiceValues <- unname(choices)
|
||||
}
|
||||
|
||||
return(list(choiceNames = as.list(choiceNames),
|
||||
choiceValues = as.list(as.character(choiceValues))))
|
||||
}
|
||||
|
||||
# generate options for radio buttons and checkbox groups (type = 'checkbox' or
|
||||
# 'radio')
|
||||
generateOptions <- function(inputId, selected, inline, type = 'checkbox',
|
||||
choiceNames, choiceValues,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
# generate a list of <input type=? [checked] />
|
||||
options <- mapply(
|
||||
choiceValues, choiceNames,
|
||||
FUN = function(value, name) {
|
||||
inputTag <- tags$input(
|
||||
type = type, name = inputId, value = value
|
||||
)
|
||||
if (value %in% selected)
|
||||
inputTag$attribs$checked <- "checked"
|
||||
|
||||
# in case, the options include UI code other than text
|
||||
# (arbitrary HTML using the tags() function or equivalent)
|
||||
pd <- processDeps(name, session)
|
||||
|
||||
# If inline, there's no wrapper div, and the label needs a class like
|
||||
# checkbox-inline.
|
||||
if (inline) {
|
||||
tags$label(class = paste0(type, "-inline"), inputTag,
|
||||
tags$span(pd$html, pd$deps))
|
||||
} else {
|
||||
tags$div(class = type, tags$label(inputTag,
|
||||
tags$span(pd$html, pd$deps)))
|
||||
}
|
||||
},
|
||||
SIMPLIFY = FALSE, USE.NAMES = FALSE
|
||||
)
|
||||
|
||||
div(class = "shiny-options-group", options)
|
||||
}
|
||||
|
||||
# True when a choice list item represents a group of related inputs.
|
||||
isGroup <- function(choice) {
|
||||
is.list(choice) ||
|
||||
!is.null(names(choice)) ||
|
||||
length(choice) > 1 ||
|
||||
length(choice) == 0
|
||||
}
|
||||
|
||||
# True when choices is a list and contains at least one group of related inputs.
|
||||
hasGroups <- function(choices) {
|
||||
is.list(choices) && any(vapply(choices, isGroup, logical(1)))
|
||||
}
|
||||
|
||||
# Assigns empty names to x if it's unnamed, and then fills any empty names with
|
||||
# the corresponding value coerced to a character(1).
|
||||
setDefaultNames <- function(x) {
|
||||
x <- asNamed(x)
|
||||
emptyNames <- names(x) == ""
|
||||
names(x)[emptyNames] <- as.character(x)[emptyNames]
|
||||
x
|
||||
}
|
||||
|
||||
# Makes a character vector out of x in a way that preserves names.
|
||||
asCharacter <- function(x) {
|
||||
stats::setNames(as.character(x), names(x))
|
||||
}
|
||||
|
||||
# Processes a "flat" set of choices, or a collection of choices not containing
|
||||
# any named groups. choices should be a list without any list children, or an
|
||||
# atomic vector. choices may be named or unnamed. Any empty names are replaced
|
||||
# with the corresponding value coerced to a character.
|
||||
processFlatChoices <- function(choices) {
|
||||
choices <- setDefaultNames(asCharacter(choices))
|
||||
as.list(choices)
|
||||
}
|
||||
|
||||
# Processes a "nested" set of choices, or a collection of choices that contains
|
||||
# one or more named groups of related choices and zero or more "flat" choices.
|
||||
# choices should be a named list, and any choice group must have a non-empty
|
||||
# name. Empty names of remaining "flat" choices are replaced with that choice's
|
||||
# value coerced to a character.
|
||||
processGroupedChoices <- function(choices) {
|
||||
# We assert choices is a list, since only a list may contain a group.
|
||||
stopifnot(is.list(choices))
|
||||
# The list might be unnamed by this point. We add default names of "" so that
|
||||
# names(choices) is not zero-length and mapply can work. Within mapply, we
|
||||
# error if any group's name is ""
|
||||
choices <- asNamed(choices)
|
||||
choices <- mapply(function(name, choice) {
|
||||
choiceIsGroup <- isGroup(choice)
|
||||
if (choiceIsGroup && name == "") {
|
||||
# If the choice is a group, and if its name is empty, produce an error. We
|
||||
# error here because the composite nature of the choice prevents us from
|
||||
# meaningfully automatically naming it. Note that while not documented,
|
||||
# groups are not necessarily lists (aka generic vectors) but can also be
|
||||
# any named atomic vector, or any atomic vector of length > 1.
|
||||
stop('All sub-lists in "choices" must be named.')
|
||||
} else if (choiceIsGroup) {
|
||||
# The choice is a group, but it is named. Process it using the same
|
||||
# function we use for "top level" choices.
|
||||
processFlatChoices(choice)
|
||||
} else {
|
||||
# The choice was not named and is not a group; it is a "leaf".
|
||||
as.character(choice)
|
||||
}
|
||||
}, names(choices), choices, SIMPLIFY = FALSE)
|
||||
# By this point, any leaves in the choices list might still have empty names,
|
||||
# so we're sure to automatically name them.
|
||||
setDefaultNames(choices)
|
||||
}
|
||||
|
||||
# Takes a vector/list/factor, and adds names (same as the value) to any entries
|
||||
# without names. Coerces all leaf nodes to `character`.
|
||||
choicesWithNames <- function(choices) {
|
||||
if (hasGroups(choices)) {
|
||||
processGroupedChoices(choices)
|
||||
} else {
|
||||
processFlatChoices(choices)
|
||||
}
|
||||
}
|
||||
261
R/insert-tab.R
Normal file
261
R/insert-tab.R
Normal file
@@ -0,0 +1,261 @@
|
||||
#' Dynamically insert/remove a tabPanel
|
||||
#'
|
||||
#' Dynamically insert or remove a [tabPanel()] (or a
|
||||
#' [navbarMenu()]) from an existing [tabsetPanel()],
|
||||
#' [navlistPanel()] or [navbarPage()].
|
||||
#'
|
||||
#' When you want to insert a new tab before or after an existing tab, you
|
||||
#' should use `insertTab`. When you want to prepend a tab (i.e. add a
|
||||
#' tab to the beginning of the `tabsetPanel`), use `prependTab`.
|
||||
#' When you want to append a tab (i.e. add a tab to the end of the
|
||||
#' `tabsetPanel`), use `appendTab`.
|
||||
#'
|
||||
#' For `navbarPage`, you can insert/remove conventional
|
||||
#' `tabPanel`s (whether at the top level or nested inside a
|
||||
#' `navbarMenu`), as well as an entire [navbarMenu()].
|
||||
#' For the latter case, `target` should be the `menuName` that
|
||||
#' you gave your `navbarMenu` when you first created it (by default,
|
||||
#' this is equal to the value of the `title` argument).
|
||||
#'
|
||||
#' @param inputId The `id` of the `tabsetPanel` (or
|
||||
#' `navlistPanel` or `navbarPage`) into which `tab` will
|
||||
#' be inserted/removed.
|
||||
#'
|
||||
#' @param tab The item to be added (must be created with `tabPanel`,
|
||||
#' or with `navbarMenu`).
|
||||
#'
|
||||
#' @param target If inserting: the `value` of an existing
|
||||
#' `tabPanel`, next to which `tab` will be added.
|
||||
#' If removing: the `value` of the `tabPanel` that
|
||||
#' you want to remove. See Details if you want to insert next to/remove
|
||||
#' an entire `navbarMenu` instead.
|
||||
#'
|
||||
#' @param position Should `tab` be added before or after the
|
||||
#' `target` tab?
|
||||
#'
|
||||
#' @param select Should `tab` be selected upon being inserted?
|
||||
#'
|
||||
#' @param session The shiny session within which to call this function.
|
||||
#'
|
||||
#' @seealso [showTab()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # example app for inserting/removing a tab
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' actionButton("add", "Add 'Dynamic' tab"),
|
||||
#' actionButton("remove", "Remove 'Foo' tab")
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' tabsetPanel(id = "tabs",
|
||||
#' tabPanel("Hello", "This is the hello tab"),
|
||||
#' tabPanel("Foo", "This is the foo tab"),
|
||||
#' tabPanel("Bar", "This is the bar tab")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$add, {
|
||||
#' insertTab(inputId = "tabs",
|
||||
#' tabPanel("Dynamic", "This a dynamically-added tab"),
|
||||
#' target = "Bar"
|
||||
#' )
|
||||
#' })
|
||||
#' observeEvent(input$remove, {
|
||||
#' removeTab(inputId = "tabs", target = "Foo")
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#'
|
||||
#' # example app for prepending/appending a navbarMenu
|
||||
#' ui <- navbarPage("Navbar page", id = "tabs",
|
||||
#' tabPanel("Home",
|
||||
#' actionButton("prepend", "Prepend a navbarMenu"),
|
||||
#' actionButton("append", "Append a navbarMenu")
|
||||
#' )
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$prepend, {
|
||||
#' id <- paste0("Dropdown", input$prepend, "p")
|
||||
#' prependTab(inputId = "tabs",
|
||||
#' navbarMenu(id,
|
||||
#' tabPanel("Drop1", paste("Drop1 page from", id)),
|
||||
#' tabPanel("Drop2", paste("Drop2 page from", id)),
|
||||
#' "------",
|
||||
#' "Header",
|
||||
#' tabPanel("Drop3", paste("Drop3 page from", id))
|
||||
#' )
|
||||
#' )
|
||||
#' })
|
||||
#' observeEvent(input$append, {
|
||||
#' id <- paste0("Dropdown", input$append, "a")
|
||||
#' appendTab(inputId = "tabs",
|
||||
#' navbarMenu(id,
|
||||
#' tabPanel("Drop1", paste("Drop1 page from", id)),
|
||||
#' tabPanel("Drop2", paste("Drop2 page from", id)),
|
||||
#' "------",
|
||||
#' "Header",
|
||||
#' tabPanel("Drop3", paste("Drop3 page from", id))
|
||||
#' )
|
||||
#' )
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' }
|
||||
#' @export
|
||||
insertTab <- function(inputId, tab, target = NULL,
|
||||
position = c("after", "before"), select = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
bslib::nav_insert(
|
||||
inputId, tab, target,
|
||||
match.arg(position), select, session
|
||||
)
|
||||
}
|
||||
|
||||
#' @param menuName This argument should only be used when you want to
|
||||
#' prepend (or append) `tab` to the beginning (or end) of an
|
||||
#' existing [navbarMenu()] (which must itself be part of
|
||||
#' an existing [navbarPage()]). In this case, this argument
|
||||
#' should be the `menuName` that you gave your `navbarMenu`
|
||||
#' when you first created it (by default, this is equal to the value
|
||||
#' of the `title` argument). Note that you still need to set the
|
||||
#' `inputId` argument to whatever the `id` of the parent
|
||||
#' `navbarPage` is. If `menuName` is left as `NULL`,
|
||||
#' `tab` will be prepended (or appended) to whatever
|
||||
#' `inputId` is.
|
||||
#'
|
||||
#' @rdname insertTab
|
||||
#' @export
|
||||
prependTab <- function(inputId, tab, select = FALSE, menuName = NULL,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
bslib::nav_prepend(inputId, tab, menu_title = menuName, select = select, session = session)
|
||||
}
|
||||
|
||||
#' @rdname insertTab
|
||||
#' @export
|
||||
appendTab <- function(inputId, tab, select = FALSE, menuName = NULL,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
bslib::nav_append(inputId, tab, menu_title = menuName, select = select, session = session)
|
||||
}
|
||||
|
||||
#' @rdname insertTab
|
||||
#' @export
|
||||
removeTab <- function(inputId, target,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
bslib::nav_remove(inputId, target, session)
|
||||
}
|
||||
|
||||
|
||||
#' Dynamically hide/show a tabPanel
|
||||
#'
|
||||
#' Dynamically hide or show a [tabPanel()] (or a
|
||||
#' [navbarMenu()])from an existing [tabsetPanel()],
|
||||
#' [navlistPanel()] or [navbarPage()].
|
||||
#'
|
||||
#' For `navbarPage`, you can hide/show conventional
|
||||
#' `tabPanel`s (whether at the top level or nested inside a
|
||||
#' `navbarMenu`), as well as an entire [navbarMenu()].
|
||||
#' For the latter case, `target` should be the `menuName` that
|
||||
#' you gave your `navbarMenu` when you first created it (by default,
|
||||
#' this is equal to the value of the `title` argument).
|
||||
#'
|
||||
#' @param inputId The `id` of the `tabsetPanel` (or
|
||||
#' `navlistPanel` or `navbarPage`) in which to find
|
||||
#' `target`.
|
||||
#'
|
||||
#' @param target The `value` of the `tabPanel` to be
|
||||
#' hidden/shown. See Details if you want to hide/show an entire
|
||||
#' `navbarMenu` instead.
|
||||
#'
|
||||
#' @param select Should `target` be selected upon being shown?
|
||||
#'
|
||||
#' @param session The shiny session within which to call this function.
|
||||
#'
|
||||
#' @seealso [insertTab()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- navbarPage("Navbar page", id = "tabs",
|
||||
#' tabPanel("Home",
|
||||
#' actionButton("hideTab", "Hide 'Foo' tab"),
|
||||
#' actionButton("showTab", "Show 'Foo' tab"),
|
||||
#' actionButton("hideMenu", "Hide 'More' navbarMenu"),
|
||||
#' actionButton("showMenu", "Show 'More' navbarMenu")
|
||||
#' ),
|
||||
#' tabPanel("Foo", "This is the foo tab"),
|
||||
#' tabPanel("Bar", "This is the bar tab"),
|
||||
#' navbarMenu("More",
|
||||
#' tabPanel("Table", "Table page"),
|
||||
#' tabPanel("About", "About page"),
|
||||
#' "------",
|
||||
#' "Even more!",
|
||||
#' tabPanel("Email", "Email page")
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$hideTab, {
|
||||
#' hideTab(inputId = "tabs", target = "Foo")
|
||||
#' })
|
||||
#'
|
||||
#' observeEvent(input$showTab, {
|
||||
#' showTab(inputId = "tabs", target = "Foo")
|
||||
#' })
|
||||
#'
|
||||
#' observeEvent(input$hideMenu, {
|
||||
#' hideTab(inputId = "tabs", target = "More")
|
||||
#' })
|
||||
#'
|
||||
#' observeEvent(input$showMenu, {
|
||||
#' showTab(inputId = "tabs", target = "More")
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @export
|
||||
showTab <- function(inputId, target, select = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
force(target)
|
||||
|
||||
if (select) updateTabsetPanel(session, inputId, selected = target)
|
||||
inputId <- session$ns(inputId)
|
||||
|
||||
callback <- function() {
|
||||
session$sendChangeTabVisibility(
|
||||
inputId = inputId,
|
||||
target = target,
|
||||
type = "show"
|
||||
)
|
||||
}
|
||||
session$onFlush(callback, once = TRUE)
|
||||
}
|
||||
|
||||
#' @rdname showTab
|
||||
#' @export
|
||||
hideTab <- function(inputId, target,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
force(target)
|
||||
inputId <- session$ns(inputId)
|
||||
|
||||
callback <- function() {
|
||||
session$sendChangeTabVisibility(
|
||||
inputId = inputId,
|
||||
target = target,
|
||||
type = "hide"
|
||||
)
|
||||
}
|
||||
session$onFlush(callback, once = TRUE)
|
||||
}
|
||||
140
R/insert-ui.R
Normal file
140
R/insert-ui.R
Normal file
@@ -0,0 +1,140 @@
|
||||
#' Insert and remove UI objects
|
||||
#'
|
||||
#' These functions allow you to dynamically add and remove arbitrary UI
|
||||
#' into your app, whenever you want, as many times as you want.
|
||||
#' Unlike [renderUI()], the UI generated with `insertUI()` is persistent:
|
||||
#' once it's created, it stays there until removed by `removeUI()`. Each
|
||||
#' new call to `insertUI()` creates more UI objects, in addition to
|
||||
#' the ones already there (all independent from one another). To
|
||||
#' update a part of the UI (ex: an input object), you must use the
|
||||
#' appropriate `render` function or a customized `reactive`
|
||||
#' function.
|
||||
#'
|
||||
#' It's particularly useful to pair `removeUI` with `insertUI()`, but there is
|
||||
#' no restriction on what you can use it on. Any element that can be selected
|
||||
#' through a jQuery selector can be removed through this function.
|
||||
#'
|
||||
#' @param selector A string that is accepted by jQuery's selector
|
||||
#' (i.e. the string `s` to be placed in a `$(s)` jQuery call).
|
||||
#'
|
||||
#' For `insertUI()` this determines the element(s) relative to which you
|
||||
#' want to insert your UI object. For `removeUI()` this determine the
|
||||
#' element(s) to be removed. If you want to remove a Shiny input or output,
|
||||
#' note that many of these are wrapped in `<div>`s, so you may need to use a
|
||||
#' somewhat complex selector --- see the Examples below. (Alternatively, you
|
||||
#' could also wrap the inputs/outputs that you want to be able to remove
|
||||
#' easily in a `<div>` with an id.)
|
||||
#' @param where Where your UI object should go relative to the selector:
|
||||
#' \describe{
|
||||
#' \item{`beforeBegin`}{Before the selector element itself}
|
||||
#' \item{`afterBegin`}{Just inside the selector element, before its
|
||||
#' first child}
|
||||
#' \item{`beforeEnd`}{Just inside the selector element, after its
|
||||
#' last child (default)}
|
||||
#' \item{`afterEnd`}{After the selector element itself}
|
||||
#' }
|
||||
#' Adapted from <https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML>.
|
||||
#' @param ui The UI object you want to insert. This can be anything that
|
||||
#' you usually put inside your apps's `ui` function. If you're inserting
|
||||
#' multiple elements in one call, make sure to wrap them in either a
|
||||
#' `tagList()` or a `tags$div()` (the latter option has the
|
||||
#' advantage that you can give it an `id` to make it easier to
|
||||
#' reference or remove it later on). If you want to insert raw html, use
|
||||
#' `ui = HTML()`.
|
||||
#' @param multiple In case your selector matches more than one element,
|
||||
#' `multiple` determines whether Shiny should insert the UI object
|
||||
#' relative to all matched elements or just relative to the first
|
||||
#' matched element (default).
|
||||
#' @param immediate Whether the UI object should be immediately inserted
|
||||
#' or removed, or whether Shiny should wait until all outputs have been
|
||||
#' updated and all observers have been run (default).
|
||||
#' @param session The shiny session. Advanced use only.
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' # Define UI
|
||||
#' ui <- fluidPage(
|
||||
#' actionButton("add", "Add UI")
|
||||
#' )
|
||||
#'
|
||||
#' # Server logic
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$add, {
|
||||
#' insertUI(
|
||||
#' selector = "#add",
|
||||
#' where = "afterEnd",
|
||||
#' ui = textInput(paste0("txt", input$add),
|
||||
#' "Insert some text")
|
||||
#' )
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' # Complete app with UI and server components
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' if (interactive()) {
|
||||
#' # Define UI
|
||||
#' ui <- fluidPage(
|
||||
#' actionButton("rmv", "Remove UI"),
|
||||
#' textInput("txt", "This is no longer useful")
|
||||
#' )
|
||||
#'
|
||||
#' # Server logic
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$rmv, {
|
||||
#' removeUI(
|
||||
#' selector = "div:has(> #txt)"
|
||||
#' )
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' # Complete app with UI and server components
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
insertUI <- function(selector,
|
||||
where = c("beforeBegin", "afterBegin", "beforeEnd", "afterEnd"),
|
||||
ui,
|
||||
multiple = FALSE,
|
||||
immediate = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(selector)
|
||||
force(ui)
|
||||
force(session)
|
||||
force(multiple)
|
||||
if (missing(where)) where <- "beforeEnd"
|
||||
where <- match.arg(where)
|
||||
|
||||
callback <- function() {
|
||||
session$sendInsertUI(selector = selector,
|
||||
multiple = multiple,
|
||||
where = where,
|
||||
content = processDeps(ui, session))
|
||||
}
|
||||
|
||||
if (!immediate) session$onFlushed(callback, once = TRUE)
|
||||
else callback()
|
||||
}
|
||||
|
||||
|
||||
#' @rdname insertUI
|
||||
#' @export
|
||||
removeUI <- function(selector,
|
||||
multiple = FALSE,
|
||||
immediate = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(selector)
|
||||
force(multiple)
|
||||
force(session)
|
||||
|
||||
callback <- function() {
|
||||
session$sendRemoveUI(selector = selector,
|
||||
multiple = multiple)
|
||||
}
|
||||
|
||||
if (!immediate) session$onFlushed(callback, once = TRUE)
|
||||
else callback()
|
||||
}
|
||||
116
R/jqueryui.R
Normal file
116
R/jqueryui.R
Normal file
@@ -0,0 +1,116 @@
|
||||
#' Panel with absolute positioning
|
||||
#'
|
||||
#' Creates a panel whose contents are absolutely positioned.
|
||||
#'
|
||||
#' The `absolutePanel` function creates a `<div>` tag whose CSS
|
||||
#' position is set to `absolute` (or fixed if `fixed = TRUE`). The way
|
||||
#' absolute positioning works in HTML is that absolute coordinates are specified
|
||||
#' relative to its nearest parent element whose position is not set to
|
||||
#' `static` (which is the default), and if no such parent is found, then
|
||||
#' relative to the page borders. If you're not sure what that means, just keep
|
||||
#' in mind that you may get strange results if you use `absolutePanel` from
|
||||
#' inside of certain types of panels.
|
||||
#'
|
||||
#' The `fixedPanel` function is the same as `absolutePanel` with
|
||||
#' `fixed = TRUE`.
|
||||
#'
|
||||
#' The position (`top`, `left`, `right`, `bottom`) and size
|
||||
#' (`width`, `height`) parameters are all optional, but you should
|
||||
#' specify exactly two of `top`, `bottom`, and `height` and
|
||||
#' exactly two of `left`, `right`, and `width` for predictable
|
||||
#' results.
|
||||
#'
|
||||
#' Like most other distance parameters in Shiny, the position and size
|
||||
#' parameters take a number (interpreted as pixels) or a valid CSS size string,
|
||||
#' such as `"100px"` (100 pixels) or `"25%"`.
|
||||
#'
|
||||
#' For arcane HTML reasons, to have the panel fill the page or parent you should
|
||||
#' specify `0` for `top`, `left`, `right`, and `bottom`
|
||||
#' rather than the more obvious `width = "100%"` and `height =
|
||||
#' "100%"`.
|
||||
#'
|
||||
#' @param ... Attributes (named arguments) or children (unnamed arguments) that
|
||||
#' should be included in the panel.
|
||||
#'
|
||||
#' @param top Distance between the top of the panel, and the top of the page or
|
||||
#' parent container.
|
||||
#' @param left Distance between the left side of the panel, and the left of the
|
||||
#' page or parent container.
|
||||
#' @param right Distance between the right side of the panel, and the right of
|
||||
#' the page or parent container.
|
||||
#' @param bottom Distance between the bottom of the panel, and the bottom of the
|
||||
#' page or parent container.
|
||||
#' @param width Width of the panel.
|
||||
#' @param height Height of the panel.
|
||||
#' @param draggable If `TRUE`, allows the user to move the panel by
|
||||
#' clicking and dragging.
|
||||
#' @param fixed Positions the panel relative to the browser window and prevents
|
||||
#' it from being scrolled with the rest of the page.
|
||||
#' @param cursor The type of cursor that should appear when the user mouses over
|
||||
#' the panel. Use `"move"` for a north-east-south-west icon,
|
||||
#' `"default"` for the usual cursor arrow, or `"inherit"` for the
|
||||
#' usual cursor behavior (including changing to an I-beam when the cursor is
|
||||
#' over text). The default is `"auto"`, which is equivalent to
|
||||
#' `ifelse(draggable, "move", "inherit")`.
|
||||
#' @return An HTML element or list of elements.
|
||||
#' @export
|
||||
absolutePanel <- function(...,
|
||||
top = NULL, left = NULL, right = NULL, bottom = NULL,
|
||||
width = NULL, height = NULL,
|
||||
draggable = FALSE, fixed = FALSE,
|
||||
cursor = c('auto', 'move', 'default', 'inherit')) {
|
||||
cssProps <- list(
|
||||
top = top,
|
||||
left = left,
|
||||
right = right,
|
||||
bottom = bottom,
|
||||
width = width,
|
||||
height = height
|
||||
)
|
||||
cssProps <- cssProps[!sapply(cssProps, is.null)]
|
||||
cssProps <- sapply(cssProps, validateCssUnit)
|
||||
cssProps[['position']] <- ifelse(fixed, 'fixed', 'absolute')
|
||||
cssProps[['cursor']] <- match.arg(cursor)
|
||||
if (identical(cssProps[['cursor']], 'auto'))
|
||||
cssProps[['cursor']] <- ifelse(draggable, 'move', 'inherit')
|
||||
|
||||
style <- paste(paste(names(cssProps), cssProps, sep = ':', collapse = ';'), ';', sep='')
|
||||
divTag <- tags$div(style=style, ...)
|
||||
|
||||
if (identical(draggable, FALSE)) {
|
||||
return(divTag)
|
||||
}
|
||||
|
||||
# Add Shiny inputs and htmlwidgets to 'non-draggable' elements
|
||||
# Cf. https://api.jqueryui.com/draggable/#option-cancel
|
||||
dragOpts <- '{cancel: ".shiny-input-container,.html-widget,input,textarea,button,select,option"}'
|
||||
dragJS <- sprintf('$(".draggable").draggable(%s);', dragOpts)
|
||||
tagList(
|
||||
tagAppendAttributes(divTag, class='draggable'),
|
||||
jqueryuiDependency(),
|
||||
tags$script(HTML(dragJS))
|
||||
)
|
||||
}
|
||||
|
||||
#' @rdname absolutePanel
|
||||
#' @export
|
||||
fixedPanel <- function(...,
|
||||
top = NULL, left = NULL, right = NULL, bottom = NULL,
|
||||
width = NULL, height = NULL,
|
||||
draggable = FALSE,
|
||||
cursor = c('auto', 'move', 'default', 'inherit')) {
|
||||
absolutePanel(..., top=top, left=left, right=right, bottom=bottom,
|
||||
width=width, height=height, draggable=draggable, cursor=match.arg(cursor),
|
||||
fixed=TRUE)
|
||||
}
|
||||
|
||||
|
||||
jqueryuiDependency <- function() {
|
||||
htmlDependency(
|
||||
"jqueryui",
|
||||
version_jqueryui,
|
||||
src = "www/shared/jqueryui",
|
||||
package = "shiny",
|
||||
script = "jquery-ui.min.js"
|
||||
)
|
||||
}
|
||||
81
R/knitr.R
Normal file
81
R/knitr.R
Normal file
@@ -0,0 +1,81 @@
|
||||
#' Knitr S3 methods
|
||||
#'
|
||||
#' These S3 methods are necessary to help Shiny applications and UI chunks embed
|
||||
#' themselves in knitr/rmarkdown documents.
|
||||
#'
|
||||
#' @name knitr_methods
|
||||
#' @keywords internal
|
||||
#' @param x Object to knit_print
|
||||
#' @param ... Additional knit_print arguments
|
||||
NULL
|
||||
|
||||
# If there's an R Markdown runtime option set but it isn't set to Shiny, then
|
||||
# return a warning indicating the runtime is inappropriate for this object.
|
||||
# Returns NULL in all other cases.
|
||||
shiny_rmd_warning <- function() {
|
||||
runtime <- knitr::opts_knit$get("rmarkdown.runtime")
|
||||
if (!is.null(runtime) && runtime != "shiny")
|
||||
# note that the RStudio IDE checks for this specific string to detect Shiny
|
||||
# applications in static document
|
||||
list(structure(
|
||||
"Shiny application in a static R Markdown document",
|
||||
class = "rmd_warning"))
|
||||
else
|
||||
NULL
|
||||
}
|
||||
|
||||
#' @rdname knitr_methods
|
||||
knit_print.shiny.appobj <- function(x, ...) {
|
||||
opts <- x$options %||% list()
|
||||
width <- if (is.null(opts$width)) "100%" else opts$width
|
||||
height <- if (is.null(opts$height)) "400" else opts$height
|
||||
|
||||
runtime <- knitr::opts_knit$get("rmarkdown.runtime")
|
||||
if (!is.null(runtime) && runtime != "shiny") {
|
||||
# If not rendering to a Shiny document, create a box exactly the same
|
||||
# dimensions as the Shiny app would have had (so the document continues to
|
||||
# flow as it would have with the app), and display a diagnostic message
|
||||
width <- validateCssUnit(width)
|
||||
height <- validateCssUnit(height)
|
||||
output <- tags$div(
|
||||
style=paste("width:", width, "; height:", height, "; text-align: center;",
|
||||
"box-sizing: border-box;", "-moz-box-sizing: border-box;",
|
||||
"-webkit-box-sizing: border-box;"),
|
||||
class="muted well",
|
||||
"Shiny applications not supported in static R Markdown documents")
|
||||
}
|
||||
else {
|
||||
path <- addSubApp(x)
|
||||
output <- deferredIFrame(path, width, height)
|
||||
}
|
||||
|
||||
# If embedded Shiny apps ever have JS/CSS dependencies (like pym.js) we'll
|
||||
# need to grab those and put them in meta, like in knit_print.shiny.tag. But
|
||||
# for now it's not an issue, so just return the HTML and warning.
|
||||
|
||||
knitr::asis_output(htmlPreserve(format(output, indent=FALSE)),
|
||||
meta = shiny_rmd_warning(), cacheable = FALSE)
|
||||
}
|
||||
|
||||
# Let us use a nicer syntax in knitr chunks than literally
|
||||
# calling output$value <- renderFoo(...) and fooOutput().
|
||||
#' @rdname knitr_methods
|
||||
#' @param inline Whether the object is printed inline.
|
||||
knit_print.shiny.render.function <- function(x, ..., inline = FALSE) {
|
||||
x <- htmltools::as.tags(x, inline = inline)
|
||||
output <- knitr::knit_print(tagList(x), ..., inline = inline)
|
||||
attr(output, "knit_cacheable") <- FALSE
|
||||
attr(output, "knit_meta") <- append(attr(output, "knit_meta"),
|
||||
shiny_rmd_warning())
|
||||
output
|
||||
}
|
||||
|
||||
# Lets us drop reactive expressions directly into a knitr chunk and have the
|
||||
# value printed out! Nice for teaching if nothing else.
|
||||
#' @rdname knitr_methods
|
||||
knit_print.reactive <- function(x, ..., inline = FALSE) {
|
||||
renderFunc <- if (inline) renderText else renderPrint
|
||||
knitr::knit_print(renderFunc({
|
||||
x()
|
||||
}), ..., inline = inline)
|
||||
}
|
||||
87
R/map.R
87
R/map.R
@@ -1,74 +1,59 @@
|
||||
# TESTS
|
||||
# Simple set/get
|
||||
# Simple remove
|
||||
# Simple containsKey
|
||||
# Simple keys
|
||||
# Simple values
|
||||
# Simple clear
|
||||
# Get of unknown key returns NULL
|
||||
# Remove of unknown key does nothing
|
||||
# Setting a key twice always results in last-one-wins
|
||||
# /TESTS
|
||||
Map <- setRefClass(
|
||||
Map <- R6Class(
|
||||
'Map',
|
||||
fields = list(
|
||||
.env = 'environment'
|
||||
),
|
||||
methods = list(
|
||||
portable = FALSE,
|
||||
public = list(
|
||||
initialize = function() {
|
||||
.env <<- new.env(parent=emptyenv())
|
||||
private$map <<- fastmap()
|
||||
},
|
||||
get = function(key) {
|
||||
if (.self$containsKey(key))
|
||||
return(base::get(key, pos=.env, inherits=F))
|
||||
else
|
||||
return(NULL)
|
||||
map$get(key)
|
||||
},
|
||||
set = function(key, value) {
|
||||
assign(key, value, pos=.env, inherits=F)
|
||||
return(value)
|
||||
map$set(key, value)
|
||||
value
|
||||
},
|
||||
mget = function(keys) {
|
||||
map$mget(keys)
|
||||
},
|
||||
mset = function(...) {
|
||||
map$mset(...)
|
||||
},
|
||||
remove = function(key) {
|
||||
if (.self$containsKey(key)) {
|
||||
result <- .self$get(key)
|
||||
rm(list = key, pos=.env, inherits=F)
|
||||
return(result)
|
||||
}
|
||||
return(NULL)
|
||||
if (!map$has(key))
|
||||
return(NULL)
|
||||
|
||||
result <- map$get(key)
|
||||
map$remove(key)
|
||||
result
|
||||
},
|
||||
containsKey = function(key) {
|
||||
exists(key, where=.env, inherits=F)
|
||||
map$has(key)
|
||||
},
|
||||
keys = function() {
|
||||
ls(envir=.env, all.names=T)
|
||||
keys = function(sort = FALSE) {
|
||||
map$keys(sort = sort)
|
||||
},
|
||||
values = function() {
|
||||
mget(.self$keys(), envir=.env, inherits=F)
|
||||
values = function(sort = FALSE) {
|
||||
map$as_list(sort = sort)
|
||||
},
|
||||
clear = function() {
|
||||
.env <<- new.env(parent=emptyenv())
|
||||
invisible(NULL)
|
||||
map$reset()
|
||||
},
|
||||
size = function() {
|
||||
length(.env)
|
||||
map$size()
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
map = NULL
|
||||
)
|
||||
)
|
||||
|
||||
`[.Map` <- function(map, name) {
|
||||
map$get(name)
|
||||
#' @export
|
||||
as.list.Map <- function(x, ...) {
|
||||
x$values()
|
||||
}
|
||||
|
||||
`[<-.Map` <- function(map, name, value) {
|
||||
map$set(name, value)
|
||||
return(map)
|
||||
}
|
||||
|
||||
as.list.Map <- function(map) {
|
||||
sapply(map$keys(),
|
||||
map$get,
|
||||
simplify=F)
|
||||
}
|
||||
length.Map <- function(map) {
|
||||
map$size()
|
||||
#' @export
|
||||
length.Map <- function(x) {
|
||||
x$size()
|
||||
}
|
||||
|
||||
88
R/middleware-shiny.R
Normal file
88
R/middleware-shiny.R
Normal file
@@ -0,0 +1,88 @@
|
||||
#' @include globals.R
|
||||
NULL
|
||||
|
||||
reactLogHandler <- function(req) {
|
||||
if (! rLog$isLogging()) {
|
||||
if (
|
||||
identical(req$PATH_INFO, "/reactlog/mark") ||
|
||||
identical(req$PATH_INFO, "/reactlog")
|
||||
) {
|
||||
# is not logging, but is a reactlog path...
|
||||
|
||||
return(
|
||||
httpResponse(
|
||||
# Not Implemented
|
||||
# - The server either does not recognize the request method, or it lacks the ability to fulfil the request.
|
||||
status = 501,
|
||||
content_type = "text/plain; charset=utf-8",
|
||||
content = "To enable reactlog, set the following option before running the application: \n\noptions(shiny.reactlog = TRUE)"
|
||||
)
|
||||
)
|
||||
|
||||
} else {
|
||||
# continue on like normal
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (identical(req$PATH_INFO, "/reactlog/mark")) {
|
||||
sessionToken <- parseQueryString(req$QUERY_STRING)$s
|
||||
shinysession <- appsByToken$get(sessionToken)
|
||||
|
||||
# log time
|
||||
withReactiveDomain(shinysession, {
|
||||
rLog$userMark(getDefaultReactiveDomain())
|
||||
})
|
||||
|
||||
return(httpResponse(
|
||||
status = 200,
|
||||
content = "marked",
|
||||
content_type = "text/plain"
|
||||
))
|
||||
|
||||
} else if (identical(req$PATH_INFO, "/reactlog")){
|
||||
|
||||
sessionToken <- parseQueryString(req$QUERY_STRING)$s
|
||||
|
||||
# `renderReactLog` will check/throw if reactlog doesn't exist
|
||||
reactlogFile <- renderReactlog(sessionToken)
|
||||
|
||||
return(httpResponse(
|
||||
status = 200,
|
||||
content = list(
|
||||
file = reactlogFile,
|
||||
owned = TRUE
|
||||
)
|
||||
))
|
||||
|
||||
} else {
|
||||
# continue on like normal
|
||||
return(NULL)
|
||||
}
|
||||
}
|
||||
|
||||
sessionHandler <- function(req) {
|
||||
path <- req$PATH_INFO
|
||||
if (is.null(path))
|
||||
return(NULL)
|
||||
|
||||
matches <- regmatches(path, regexec('^(/session/([0-9a-f]+))(/.*)$', path))
|
||||
if (length(matches[[1]]) == 0)
|
||||
return(NULL)
|
||||
|
||||
session <- matches[[1]][3]
|
||||
subpath <- matches[[1]][4]
|
||||
|
||||
shinysession <- appsByToken$get(session)
|
||||
if (is.null(shinysession))
|
||||
return(NULL)
|
||||
|
||||
subreq <- as.environment(as.list(req, all.names=TRUE))
|
||||
subreq$PATH_INFO <- subpath
|
||||
subreq$SCRIPT_NAME <- paste(subreq$SCRIPT_NAME, matches[[1]][2], sep='')
|
||||
|
||||
withReactiveDomain(shinysession, {
|
||||
shinysession$handleRequest(subreq)
|
||||
})
|
||||
}
|
||||
477
R/middleware.R
Normal file
477
R/middleware.R
Normal file
@@ -0,0 +1,477 @@
|
||||
# This file contains a general toolkit for routing and combining bits of
|
||||
# HTTP-handling logic. It is similar in spirit to Rook (and Rack, and WSGI, and
|
||||
# Connect, and...) but adds cascading and routing.
|
||||
#
|
||||
# This file is called "middleware" because that's the term used for these bits
|
||||
# of logic in these other frameworks. However, our code uses the word "handler"
|
||||
# so we'll stick to that for the rest of this document; just know that they're
|
||||
# basically the same concept.
|
||||
#
|
||||
# ## Intro to handlers
|
||||
#
|
||||
# A **handler** (or sometimes, **httpHandler**) is a function that takes a
|
||||
# `req` parameter--a request object as described in the Rook specification--and
|
||||
# returns `NULL`, or an `httpResponse`.
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
|
||||
#' Create an HTTP response object
|
||||
#'
|
||||
#' @param status HTTP status code for the response.
|
||||
#' @param content_type The value for the `Content-Type` header.
|
||||
#' @param content The body of the response, given as a single-element character
|
||||
#' vector (will be encoded as UTF-8) or a raw vector.
|
||||
#' @param headers A named list of additional headers to include. Do not include
|
||||
#' `Content-Length` (as it is automatically calculated) or `Content-Type` (the
|
||||
#' `content_type` argument is used instead).
|
||||
#'
|
||||
#' @examples
|
||||
#' httpResponse(status = 405L,
|
||||
#' content_type = "text/plain",
|
||||
#' content = "The requested method was not allowed"
|
||||
#' )
|
||||
#'
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
httpResponse <- function(status = 200L,
|
||||
content_type = "text/html; charset=UTF-8",
|
||||
content = "",
|
||||
headers = list()) {
|
||||
# Make sure it's a list, not a vector
|
||||
headers <- as.list(headers)
|
||||
if (is.null(headers$`X-UA-Compatible`))
|
||||
headers$`X-UA-Compatible` <- "IE=edge,chrome=1"
|
||||
resp <- list(status = status, content_type = content_type, content = content,
|
||||
headers = headers)
|
||||
class(resp) <- 'httpResponse'
|
||||
return(resp)
|
||||
}
|
||||
|
||||
#
|
||||
# You can think of a web application as being simply an aggregation of these
|
||||
# functions, each of which performs one kind of duty. Each handler in turn gets
|
||||
# a look at the request and can decide whether it knows how to handle it. If
|
||||
# so, it returns an `httpResponse` and processing terminates; if not, it
|
||||
# returns `NULL` and the next handler gets to execute. If the final handler
|
||||
# returns `NULL`, a 404 response should be returned.
|
||||
#
|
||||
# We have a similar construct for websockets: **websocket handlers** or
|
||||
# **wsHandlers**. These take a single `ws` argument which is the websocket
|
||||
# connection that was just opened, and they can either return `TRUE` if they
|
||||
# are handling the connection, and `NULL` to pass responsibility on to the next
|
||||
# wsHandler.
|
||||
#
|
||||
# ### Combining handlers
|
||||
#
|
||||
# Since it's so common for httpHandlers to be invoked in this "cascading"
|
||||
# fashion, we'll introduce a function that takes zero or more handlers and
|
||||
# returns a single handler. And while we're at it, making a directory of static
|
||||
# content available is such a common thing to do, we'll allow strings
|
||||
# representing paths to be used instead of handlers; any such strings we
|
||||
# encounter will be converted into `staticHandler` objects.
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
joinHandlers <- function(handlers) {
|
||||
# Zero handlers; return a null handler
|
||||
if (length(handlers) == 0)
|
||||
return(function(req) NULL)
|
||||
|
||||
# Just one handler (function)? Return it.
|
||||
if (is.function(handlers))
|
||||
return(handlers)
|
||||
|
||||
handlers <- lapply(handlers, function(h) {
|
||||
if (is.character(h))
|
||||
return(staticHandler(h))
|
||||
else
|
||||
return(h)
|
||||
})
|
||||
|
||||
# Filter out NULL
|
||||
handlers <- handlers[!sapply(handlers, is.null)]
|
||||
|
||||
if (length(handlers) == 0)
|
||||
return(function(req) NULL)
|
||||
if (length(handlers) == 1)
|
||||
return(handlers[[1]])
|
||||
|
||||
function(req) {
|
||||
for (handler in handlers) {
|
||||
response <- handler(req)
|
||||
if (!is.null(response))
|
||||
return(response)
|
||||
}
|
||||
return(NULL)
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Note that we don't have an equivalent of `joinHandlers` for wsHandlers. It's
|
||||
# easy to imagine it, we just haven't needed one.
|
||||
#
|
||||
# ### Handler routing
|
||||
#
|
||||
# Handlers do not have a built-in notion of routing. Conceptually, given a list
|
||||
# of handlers, all the handlers are peers and they all get to see every request
|
||||
# (well, up until the point that a handler returns a response).
|
||||
#
|
||||
# You could implement routing in each handler by checking the request's
|
||||
# `PATH_INFO` field, but since it's such a common need, let's make it simple by
|
||||
# introducing a `routeHandler` function. This is a handler
|
||||
# [decorator](http://en.wikipedia.org/wiki/Decorator_pattern) and it's
|
||||
# responsible for 1) filtering out requests that don't match the given route,
|
||||
# and 2) temporarily modifying the request object to take the matched part of
|
||||
# the route off of the `PATH_INFO` (and add it to the end of `SCRIPT_NAME`).
|
||||
# This way, the handler doesn't need to figure out about what part of its URL
|
||||
# path has already been matched via routing.
|
||||
#
|
||||
# (BTW, it's safe for `routeHandler` calls to nest.)
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
routeHandler <- function(prefix, handler) {
|
||||
force(prefix)
|
||||
force(handler)
|
||||
|
||||
if (identical("", prefix))
|
||||
return(handler)
|
||||
|
||||
if (length(prefix) != 1 || !isTRUE(grepl("^/[^\\]+$", prefix))) {
|
||||
stop("Invalid URL prefix \"", prefix, "\"")
|
||||
}
|
||||
|
||||
pathPattern <- paste("^\\Q", prefix, "\\E/", sep = "")
|
||||
function(req) {
|
||||
if (isTRUE(grepl(pathPattern, req$PATH_INFO))) {
|
||||
origScript <- req$SCRIPT_NAME
|
||||
origPath <- req$PATH_INFO
|
||||
on.exit({
|
||||
req$SCRIPT_NAME <- origScript
|
||||
req$PATH_INFO <- origPath
|
||||
}, add = TRUE)
|
||||
pathInfo <- substr(req$PATH_INFO, nchar(prefix)+1, nchar(req$PATH_INFO))
|
||||
req$SCRIPT_NAME <- paste(req$SCRIPT_NAME, prefix, sep = "")
|
||||
req$PATH_INFO <- pathInfo
|
||||
return(handler(req))
|
||||
} else {
|
||||
return(NULL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# We have a version for websocket handlers as well. Pity about the copy/paste
|
||||
# job.
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
routeWSHandler <- function(prefix, wshandler) {
|
||||
force(prefix)
|
||||
force(wshandler)
|
||||
|
||||
if (identical("", prefix))
|
||||
return(wshandler)
|
||||
|
||||
if (length(prefix) != 1 || !isTRUE(grepl("^/[^\\]+$", prefix))) {
|
||||
stop("Invalid URL prefix \"", prefix, "\"")
|
||||
}
|
||||
|
||||
pathPattern <- paste("^\\Q", prefix, "\\E/", sep = "")
|
||||
function(ws) {
|
||||
req <- ws$request
|
||||
if (isTRUE(grepl(pathPattern, req$PATH_INFO))) {
|
||||
origScript <- req$SCRIPT_NAME
|
||||
origPath <- req$PATH_INFO
|
||||
on.exit({
|
||||
req$SCRIPT_NAME <- origScript
|
||||
req$PATH_INFO <- origPath
|
||||
}, add = TRUE)
|
||||
pathInfo <- substr(req$PATH_INFO, nchar(prefix)+1, nchar(req$PATH_INFO))
|
||||
req$SCRIPT_NAME <- paste(req$SCRIPT_NAME, prefix, sep = "")
|
||||
req$PATH_INFO <- pathInfo
|
||||
return(wshandler(ws))
|
||||
} else {
|
||||
return(NULL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# ### Handler implementations
|
||||
#
|
||||
# Now let's actually write some handlers. Note that these functions aren't
|
||||
# *themselves* handlers, you call them and they *return* a handler. Handler
|
||||
# factory functions, if you will.
|
||||
#
|
||||
# Here's one that serves up static assets from a directory.
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
staticHandler <- function(root) {
|
||||
force(root)
|
||||
return(function(req) {
|
||||
if (!identical(req$REQUEST_METHOD, 'GET'))
|
||||
return(NULL)
|
||||
|
||||
path <- URLdecode(req$PATH_INFO)
|
||||
|
||||
if (is.null(path))
|
||||
return(httpResponse(400, content="<h1>Bad Request</h1>"))
|
||||
|
||||
if (path == '/')
|
||||
path <- '/index.html'
|
||||
|
||||
if (grepl('\\', path, fixed = TRUE))
|
||||
return(NULL)
|
||||
|
||||
abs.path <- resolve(root, path)
|
||||
if (is.null(abs.path))
|
||||
return(NULL)
|
||||
|
||||
content.type <- getContentType(abs.path)
|
||||
response.content <- readBin(abs.path, 'raw', n=file.info(abs.path)$size)
|
||||
return(httpResponse(200, content.type, response.content))
|
||||
})
|
||||
}
|
||||
|
||||
#
|
||||
# ## Handler manager
|
||||
#
|
||||
# The handler manager gives you a place to register handlers (of both http and
|
||||
# websocket varieties) and provides an httpuv-compatible set of callbacks for
|
||||
# invoking them.
|
||||
#
|
||||
# Create one of these, make zero or more calls to `addHandler` and
|
||||
# `addWSHandler` methods (order matters--first one wins!), and then pass the
|
||||
# return value of `createHttpuvApp` to httpuv's `startServer` function.
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
HandlerList <- R6Class("HandlerList",
|
||||
portable = FALSE,
|
||||
class = FALSE,
|
||||
public = list(
|
||||
handlers = list(),
|
||||
|
||||
add = function(handler, key, tail = FALSE) {
|
||||
if (!is.null(handlers[[key]]))
|
||||
stop("Key ", key, " already in use")
|
||||
newList <- structure(names=key, list(handler))
|
||||
|
||||
if (length(handlers) == 0)
|
||||
handlers <<- newList
|
||||
else if (tail)
|
||||
handlers <<- c(handlers, newList)
|
||||
else
|
||||
handlers <<- c(newList, handlers)
|
||||
},
|
||||
remove = function(key) {
|
||||
handlers[key] <<- NULL
|
||||
},
|
||||
clear = function() {
|
||||
handlers <<- list()
|
||||
},
|
||||
invoke = function(...) {
|
||||
for (handler in handlers) {
|
||||
result <- handler(...)
|
||||
if (!is.null(result))
|
||||
return(result)
|
||||
}
|
||||
return(NULL)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
HandlerManager <- R6Class("HandlerManager",
|
||||
portable = FALSE,
|
||||
class = FALSE,
|
||||
public = list(
|
||||
handlers = "HandlerList",
|
||||
wsHandlers = "HandlerList",
|
||||
|
||||
initialize = function() {
|
||||
handlers <<- HandlerList$new()
|
||||
wsHandlers <<- HandlerList$new()
|
||||
},
|
||||
|
||||
addHandler = function(handler, key, tail = FALSE) {
|
||||
handlers$add(handler, key, tail)
|
||||
},
|
||||
removeHandler = function(key) {
|
||||
handlers$remove(key)
|
||||
},
|
||||
addWSHandler = function(wsHandler, key, tail = FALSE) {
|
||||
wsHandlers$add(wsHandler, key, tail)
|
||||
},
|
||||
removeWSHandler = function(key) {
|
||||
wsHandlers$remove(key)
|
||||
},
|
||||
clear = function() {
|
||||
handlers$clear()
|
||||
wsHandlers$clear()
|
||||
},
|
||||
createHttpuvApp = function() {
|
||||
list(
|
||||
onHeaders = function(req) {
|
||||
maxSize <- getOption('shiny.maxRequestSize') %||% (5 * 1024 * 1024)
|
||||
if (maxSize <= 0)
|
||||
return(NULL)
|
||||
|
||||
reqSize <- 0
|
||||
if (length(req$CONTENT_LENGTH) > 0)
|
||||
reqSize <- as.numeric(req$CONTENT_LENGTH)
|
||||
else if (length(req$HTTP_TRANSFER_ENCODING) > 0)
|
||||
reqSize <- Inf
|
||||
|
||||
if (reqSize > maxSize) {
|
||||
return(list(status = 413L,
|
||||
headers = list('Content-Type' = 'text/plain'),
|
||||
body = 'Maximum upload size exceeded'))
|
||||
}
|
||||
else {
|
||||
return(NULL)
|
||||
}
|
||||
},
|
||||
call = .httpServer(
|
||||
function (req) {
|
||||
hybrid_chain(
|
||||
hybrid_chain(
|
||||
withCallingHandlers(withLogErrors(handlers$invoke(req)),
|
||||
error = function(cond) {
|
||||
sanitizeErrors <- getOption('shiny.sanitize.errors', FALSE)
|
||||
if (inherits(cond, 'shiny.custom.error') || !sanitizeErrors) {
|
||||
stop(cond$message, call. = FALSE)
|
||||
} else {
|
||||
stop(paste("An error has occurred. Check your logs or",
|
||||
"contact the app author for clarification."),
|
||||
call. = FALSE)
|
||||
}
|
||||
}
|
||||
),
|
||||
catch = function(err) {
|
||||
httpResponse(status = 500L,
|
||||
content_type = "text/html; charset=UTF-8",
|
||||
content = as.character(htmltools::htmlTemplate(
|
||||
system_file("template", "error.html", package = "shiny"),
|
||||
message = conditionMessage(err)
|
||||
))
|
||||
)
|
||||
}
|
||||
),
|
||||
function(resp) {
|
||||
maybeInjectAutoreload(resp)
|
||||
}
|
||||
)
|
||||
},
|
||||
loadSharedSecret()
|
||||
),
|
||||
onWSOpen = function(ws) {
|
||||
return(wsHandlers$invoke(ws))
|
||||
}
|
||||
)
|
||||
},
|
||||
.httpServer = function(handler, checkSharedSecret) {
|
||||
filter <- getOption('shiny.http.response.filter')
|
||||
if (is.null(filter))
|
||||
filter <- function(req, response) response
|
||||
|
||||
function(req) {
|
||||
if (!checkSharedSecret(req$HTTP_SHINY_SHARED_SECRET)) {
|
||||
return(list(status=403,
|
||||
body='<h1>403 Forbidden</h1><p>Shared secret mismatch</p>',
|
||||
headers=list('Content-Type' = 'text/html')))
|
||||
}
|
||||
|
||||
# Catch HEAD requests. For the purposes of handler functions, they
|
||||
# should be treated like GET. The difference is that they shouldn't
|
||||
# return a body in the http response.
|
||||
head_request <- FALSE
|
||||
if (identical(req$REQUEST_METHOD, "HEAD")) {
|
||||
head_request <- TRUE
|
||||
req$REQUEST_METHOD <- "GET"
|
||||
}
|
||||
|
||||
response <- handler(req)
|
||||
|
||||
res <- hybrid_chain(response, function(response) {
|
||||
if (is.null(response))
|
||||
response <- httpResponse(404, content="<h1>Not Found</h1>")
|
||||
|
||||
if (inherits(response, "httpResponse")) {
|
||||
headers <- as.list(response$headers)
|
||||
headers$'Content-Type' <- response$content_type
|
||||
|
||||
response <- filter(req, response)
|
||||
if (head_request) {
|
||||
|
||||
headers$`Content-Length` <- getResponseContentLength(response, deleteOwnedContent = TRUE)
|
||||
|
||||
return(list(
|
||||
status = response$status,
|
||||
body = "",
|
||||
headers = headers
|
||||
))
|
||||
} else {
|
||||
return(list(
|
||||
status = response$status,
|
||||
body = response$content,
|
||||
headers = headers
|
||||
))
|
||||
}
|
||||
|
||||
} else {
|
||||
# Assume it's a Rook-compatible response
|
||||
return(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
maybeInjectAutoreload <- function(resp) {
|
||||
if (get_devmode_option("shiny.autoreload", FALSE) &&
|
||||
isTRUE(grepl("^text/html($|;)", resp$content_type)) &&
|
||||
is.character(resp$content)) {
|
||||
|
||||
resp$content <- gsub(
|
||||
"</head>",
|
||||
"<script src=\"shared/shiny-autoreload.js\"></script>\n</head>",
|
||||
resp$content,
|
||||
fixed = TRUE
|
||||
)
|
||||
}
|
||||
|
||||
resp
|
||||
}
|
||||
|
||||
# Safely get the Content-Length of a Rook response, or NULL if the length cannot
|
||||
# be determined for whatever reason (probably malformed response$content).
|
||||
# If deleteOwnedContent is TRUE, then the function should delete response
|
||||
# content that is of the form list(file=..., owned=TRUE).
|
||||
getResponseContentLength <- function(response, deleteOwnedContent) {
|
||||
force(deleteOwnedContent)
|
||||
|
||||
result <- if (is.character(response$content) && length(response$content) == 1) {
|
||||
nchar(response$content, type = "bytes")
|
||||
} else if (is.raw(response$content)) {
|
||||
length(response$content)
|
||||
} else if (is.list(response$content) && !is.null(response$content$file)) {
|
||||
if (deleteOwnedContent && isTRUE(response$content$owned)) {
|
||||
on.exit(unlink(response$content$file, recursive = FALSE, force = FALSE), add = TRUE)
|
||||
}
|
||||
file.info(response$content$file)$size
|
||||
} else {
|
||||
warning("HEAD request for unexpected content class ", class(response$content)[[1]])
|
||||
NULL
|
||||
}
|
||||
|
||||
if (is.na(result)) {
|
||||
# Mostly for missing file case
|
||||
return(NULL)
|
||||
} else {
|
||||
return(result)
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# ## Next steps
|
||||
#
|
||||
# See server.R and middleware-shiny.R to see actual implementation and usage of
|
||||
# handlers in the context of Shiny.
|
||||
745
R/mock-session.R
Normal file
745
R/mock-session.R
Normal file
@@ -0,0 +1,745 @@
|
||||
# Promise helpers taken from:
|
||||
# https://github.com/rstudio/promises/blob/main/tests/testthat/common.R
|
||||
# Block until all pending later tasks have executed
|
||||
wait_for_it <- function() {
|
||||
while (!later::loop_empty()) {
|
||||
later::run_now(0.1)
|
||||
}
|
||||
}
|
||||
|
||||
# Block until the promise is resolved/rejected. If resolved, return the value.
|
||||
# If rejected, throw (yes throw, not return) the error.
|
||||
extract <- function(promise) {
|
||||
promise_value <- NULL
|
||||
error <- NULL
|
||||
promise %...>%
|
||||
(function(value) promise_value <<- value) %...!%
|
||||
(function(reason) error <<- reason)
|
||||
|
||||
wait_for_it()
|
||||
if (!is.null(error))
|
||||
stop(error)
|
||||
else
|
||||
promise_value
|
||||
}
|
||||
|
||||
# TODO: is there a way to get this behavior without exporting these functions? R6?
|
||||
# TODO: clientData is documented as a reactiveValues, which this is not. Is it possible that
|
||||
# users are currently assigning into clientData? That would not work as expected here.
|
||||
#' @noRd
|
||||
#' @export
|
||||
`$.mockclientdata` <- function(x, name) {
|
||||
if (name == "pixelratio") { return(1) }
|
||||
if (name == "url_protocol") { return("http:") }
|
||||
if (name == "url_hostname") { return("mocksession") }
|
||||
if (name == "url_port") { return(1234) }
|
||||
if (name == "url_pathname") { return("/mockpath") }
|
||||
if (name == "url_hash") { return("#mockhash") }
|
||||
if (name == "url_hash_initial") { return("#mockhash") }
|
||||
if (name == "url_search") { return("?mocksearch=1") }
|
||||
|
||||
clientRE <- "^output_(.+)_([^_]+)$"
|
||||
if(grepl(clientRE, name)) {
|
||||
# TODO: use proper regex group matching here instead of redundantly parsing
|
||||
el <- sub(clientRE, "\\1", name)
|
||||
att <- sub(clientRE, "\\2", name)
|
||||
|
||||
if (att == "width") {
|
||||
return(600)
|
||||
} else if (att == "height") {
|
||||
return(400)
|
||||
} else if (att == "hidden") {
|
||||
return(FALSE)
|
||||
}
|
||||
}
|
||||
warning("Unexpected clientdata attribute accessed: ", name)
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
#' @noRd
|
||||
#' @export
|
||||
`[[.mockclientdata` <- `$.mockclientdata`
|
||||
|
||||
#' @noRd
|
||||
#' @export
|
||||
`[.mockclientdata` <- function(values, name) {
|
||||
stop("Single-bracket indexing of mockclientdata is not allowed.")
|
||||
}
|
||||
|
||||
#' @noRd
|
||||
mapNames <- function(func, vals) {
|
||||
names(vals) <- vapply(names(vals), func, character(1))
|
||||
vals
|
||||
}
|
||||
|
||||
#' Returns a noop implementation of the public method `name` of ShinySession.
|
||||
#' @include shiny.R
|
||||
#' @noRd
|
||||
makeNoop <- function(name, msg = paste0(name, " is a noop.")) {
|
||||
if (!(name %in% names(ShinySession$public_methods)))
|
||||
stop(name, " is not public method of ShinySession.")
|
||||
impl <- ShinySession$public_methods[[name]]
|
||||
body(impl) <- rlang::expr({
|
||||
# Force arguments
|
||||
!!lapply(formalArgs(impl), rlang::sym)
|
||||
# Evade "no visible binding" note for reference to `private`
|
||||
(!!as.symbol("private"))$noopWarn(!!name, !!msg)
|
||||
invisible()
|
||||
})
|
||||
impl
|
||||
}
|
||||
|
||||
#' Accepts a series of symbols as arguments and generates corresponding noop
|
||||
#' implementations.
|
||||
#' @noRd
|
||||
makeWarnNoops <- function(...) {
|
||||
methods <- as.character(list(...))
|
||||
names(methods) <- methods
|
||||
lapply(methods, makeNoop)
|
||||
}
|
||||
|
||||
#' Returns an implementation of a ShinySession public method that signals an
|
||||
#' error.
|
||||
#' @include shiny.R
|
||||
#' @noRd
|
||||
makeError <- function(name, msg = paste0(name, " is for internal use only.")) {
|
||||
if (!(name %in% names(ShinySession$public_methods)))
|
||||
stop(name, " is not public method of ShinySession.")
|
||||
impl <- ShinySession$public_methods[[name]]
|
||||
body(impl) <- rlang::expr({
|
||||
base::stop(!!msg)
|
||||
})
|
||||
impl
|
||||
}
|
||||
|
||||
#' Accepts a series of named arguments. Each name corresponds to a ShinySession
|
||||
#' public method that should signal an error, and each argument corresponds to
|
||||
#' an error message.
|
||||
#' @noRd
|
||||
makeErrors <- function(...) {
|
||||
errors <- rlang::list2(...)
|
||||
mapply(makeError, names(errors), errors, USE.NAMES = TRUE, SIMPLIFY = FALSE)
|
||||
}
|
||||
|
||||
#' @noRd
|
||||
makeExtraMethods <- function() {
|
||||
c(makeWarnNoops(
|
||||
"allowReconnect",
|
||||
"decrementBusyCount",
|
||||
"doBookmark",
|
||||
"exportTestValues",
|
||||
"flushOutput",
|
||||
"getBookmarkExclude",
|
||||
"getTestSnapshotUrl",
|
||||
"incrementBusyCount",
|
||||
"manageHiddenOutputs",
|
||||
"manageInputs",
|
||||
"onBookmark",
|
||||
"onBookmarked",
|
||||
"onInputReceived",
|
||||
"onRestore",
|
||||
"onRestored",
|
||||
"outputOptions",
|
||||
"reactlog",
|
||||
# TODO Consider implementing this. Would require a new method like
|
||||
# session$getDataObj() to access in a test expression.
|
||||
"registerDataObj",
|
||||
"reload",
|
||||
"resetBrush",
|
||||
"sendBinaryMessage",
|
||||
"sendChangeTabVisibility",
|
||||
"sendCustomMessage",
|
||||
"sendInputMessage",
|
||||
"sendInsertTab",
|
||||
"sendInsertUI",
|
||||
"sendModal",
|
||||
"setCurrentTheme",
|
||||
"getCurrentTheme",
|
||||
"sendNotification",
|
||||
"sendProgress",
|
||||
"sendRemoveTab",
|
||||
"sendRemoveUI",
|
||||
"setBookmarkExclude",
|
||||
"setShowcase",
|
||||
"showProgress",
|
||||
"updateQueryString"
|
||||
), makeErrors(
|
||||
`@uploadEnd` = "for internal use only",
|
||||
`@uploadInit` = "for internal use only",
|
||||
createBookmarkObservers = "for internal use only",
|
||||
dispatch = "for internal use only",
|
||||
handleRequest = "for internal use only",
|
||||
requestFlush = "for internal use only",
|
||||
startTiming = "for internal use only",
|
||||
wsClosed = "for internal use only"
|
||||
))
|
||||
}
|
||||
|
||||
#' @description Adds generated instance methods to a MockShinySession instance.
|
||||
#' Note that `lock_objects = FALSE` must be set in the call to `R6Class()`
|
||||
#' that produced the generator object of the instance.
|
||||
#' @param instance instance of an R6 object, generally a `MockShinySession`.
|
||||
#' @param methods named list of method names to method implementation functions.
|
||||
#' In our typical usage, each function is derived from a public method of
|
||||
#' `ShinySession`. The environment of each implementation function is set to
|
||||
#' `instance$.__enclos_env` before the method is added.
|
||||
#' @noRd
|
||||
addGeneratedInstanceMethods <- function(instance, methods = makeExtraMethods()) {
|
||||
mapply(function(name, impl) {
|
||||
environment(impl) <- instance$.__enclos_env__
|
||||
instance[[name]] <- impl
|
||||
}, names(methods), methods)
|
||||
}
|
||||
|
||||
#' Mock Shiny Session
|
||||
#'
|
||||
#' @description An R6 class suitable for testing purposes. Simulates, to the
|
||||
#' extent possible, the behavior of the `ShinySession` class. The `session`
|
||||
#' parameter provided to Shiny server functions and modules is an instance of
|
||||
#' a `ShinySession` in normal operation.
|
||||
#'
|
||||
#' Most kinds of module and server testing do not require this class be
|
||||
#' instantiated manually. See instead [testServer()].
|
||||
#'
|
||||
#' In order to support advanced usage, instances of `MockShinySession` are
|
||||
#' **unlocked** so that public methods and fields of instances may be
|
||||
#' modified. For example, in order to test authentication workflows, the
|
||||
#' `user` or `groups` fields may be overridden. Modified instances of
|
||||
#' `MockShinySession` may then be passed explicitly as the `session` argument
|
||||
#' of [testServer()].
|
||||
#'
|
||||
#' @include timer.R
|
||||
#' @export
|
||||
MockShinySession <- R6Class(
|
||||
'MockShinySession',
|
||||
portable = FALSE,
|
||||
lock_objects = FALSE,
|
||||
public = list(
|
||||
#' @field env The environment associated with the session.
|
||||
env = NULL,
|
||||
#' @field returned The value returned by the module under test.
|
||||
returned = NULL,
|
||||
#' @field singletons Hardcoded as empty. Needed for rendering HTML (i.e. renderUI).
|
||||
singletons = character(0),
|
||||
#' @field clientData Mock client data that always returns a size for plots.
|
||||
clientData = structure(list(), class="mockclientdata"),
|
||||
#' @field output The shinyoutputs associated with the session.
|
||||
output = NULL,
|
||||
#' @field input The reactive inputs associated with the session.
|
||||
input = NULL,
|
||||
#' @field userData An environment initialized as empty.
|
||||
userData = NULL,
|
||||
#' @field progressStack A stack of progress objects.
|
||||
progressStack = 'Stack',
|
||||
#' @field token On a real `ShinySession`, used to identify this instance in URLs.
|
||||
token = 'character',
|
||||
#' @field cache The session cache object.
|
||||
cache = NULL,
|
||||
#' @field appcache The app cache object.
|
||||
appcache = NULL,
|
||||
#' @field restoreContext Part of bookmarking support in a real
|
||||
#' `ShinySession` but always `NULL` for a `MockShinySession`.
|
||||
restoreContext = NULL,
|
||||
#' @field groups Character vector of groups associated with an authenticated
|
||||
#' user. Always `NULL` for a `MockShinySesion`.
|
||||
groups = NULL,
|
||||
#' @field user The username of an authenticated user. Always `NULL` for a
|
||||
#' `MockShinySession`.
|
||||
user = NULL,
|
||||
#' @field options A list containing session-level shinyOptions.
|
||||
options = NULL,
|
||||
|
||||
#' @description Create a new MockShinySession.
|
||||
initialize = function() {
|
||||
private$.input <- ReactiveValues$new(dedupe = FALSE, label = "input")
|
||||
private$flushCBs <- Callbacks$new()
|
||||
private$flushedCBs <- Callbacks$new()
|
||||
private$endedCBs <- Callbacks$new()
|
||||
|
||||
private$file_generators <- fastmap()
|
||||
|
||||
private$timer <- MockableTimerCallbacks$new()
|
||||
self$progressStack <- fastmap::faststack()
|
||||
|
||||
self$userData <- new.env(parent=emptyenv())
|
||||
|
||||
# create output
|
||||
out <- .createOutputWriter(self)
|
||||
class(out) <- "shinyoutput"
|
||||
self$output <- out
|
||||
|
||||
# Create a read-only copy of the inputs reactive.
|
||||
self$input <- .createReactiveValues(private$.input, readonly = TRUE)
|
||||
|
||||
self$token <- createUniqueId(16)
|
||||
|
||||
# Copy app-level options
|
||||
self$options <- getCurrentAppState()$options
|
||||
|
||||
self$cache <- cachem::cache_mem()
|
||||
self$appcache <- cachem::cache_mem()
|
||||
|
||||
# Adds various generated noop and error-producing method implementations.
|
||||
# Note that noop methods can be configured to produce warnings by setting
|
||||
# the option shiny.mocksession.warn = TRUE; see $noopWarn() for details.
|
||||
addGeneratedInstanceMethods(self)
|
||||
},
|
||||
#' @description Define a callback to be invoked before a reactive flush
|
||||
#' @param fun The function to invoke
|
||||
#' @param once If `TRUE`, will only run once. Otherwise, will run every time reactives are flushed.
|
||||
onFlush = function(fun, once=TRUE) {
|
||||
if (!isTRUE(once)) {
|
||||
return(private$flushCBs$register(fun))
|
||||
} else {
|
||||
dereg <- private$flushCBs$register(function() {
|
||||
dereg()
|
||||
fun()
|
||||
})
|
||||
return(dereg)
|
||||
}
|
||||
},
|
||||
#' @description Define a callback to be invoked after a reactive flush
|
||||
#' @param fun The function to invoke
|
||||
#' @param once If `TRUE`, will only run once. Otherwise, will run every time reactives are flushed.
|
||||
onFlushed = function(fun, once=TRUE) {
|
||||
if (!isTRUE(once)) {
|
||||
return(private$flushedCBs$register(fun))
|
||||
} else {
|
||||
dereg <- private$flushedCBs$register(function() {
|
||||
dereg()
|
||||
fun()
|
||||
})
|
||||
return(dereg)
|
||||
}
|
||||
},
|
||||
#' @description Define a callback to be invoked when the session ends
|
||||
#' @param sessionEndedCallback The callback to invoke when the session has ended.
|
||||
onEnded = function(sessionEndedCallback) {
|
||||
private$endedCBs$register(sessionEndedCallback)
|
||||
},
|
||||
|
||||
#' @description Returns `FALSE` if the session has not yet been closed
|
||||
isEnded = function(){ private$was_closed },
|
||||
#' @description Returns `FALSE` if the session has not yet been closed
|
||||
isClosed = function(){ private$was_closed },
|
||||
#' @description Closes the session
|
||||
close = function(){
|
||||
for (output in private$output) {
|
||||
output$suspend()
|
||||
}
|
||||
withReactiveDomain(self, {
|
||||
private$endedCBs$invoke(onError = printError, ..stacktraceon = TRUE)
|
||||
})
|
||||
private$was_closed <- TRUE
|
||||
},
|
||||
|
||||
#FIXME: this is wrong. Will need to be more complex.
|
||||
#' @description Unsophisticated mock implementation that merely invokes
|
||||
# the given callback immediately.
|
||||
#' @param callback The callback to be invoked.
|
||||
cycleStartAction = function(callback){ callback() },
|
||||
|
||||
#' @description Base64-encode the given file. Needed for image rendering.
|
||||
#' @param name Not used
|
||||
#' @param file The file to be encoded
|
||||
#' @param contentType The content type of the base64-encoded string
|
||||
fileUrl = function(name, file, contentType='application/octet-stream') {
|
||||
bytes <- file.info(file)$size
|
||||
if (is.na(bytes))
|
||||
return(NULL)
|
||||
|
||||
fileData <- readBin(file, 'raw', n=bytes)
|
||||
b64 <- rawToBase64(fileData)
|
||||
return(paste('data:', contentType, ';base64,', b64, sep=''))
|
||||
},
|
||||
|
||||
#' @description Sets reactive values associated with the `session$inputs`
|
||||
#' object and flushes the reactives.
|
||||
#' @param ... The inputs to set. These arguments are processed with
|
||||
#' [rlang::list2()] and so are _[dynamic][rlang::dyn-dots]_. Input names
|
||||
#' may not be duplicated.
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' session$setInputs(x=1, y=2)
|
||||
#' }
|
||||
setInputs = function(...) {
|
||||
vals <- rlang::dots_list(..., .homonyms = "error")
|
||||
mapply(names(vals), vals, FUN = function(name, value) {
|
||||
private$.input$set(name, value)
|
||||
})
|
||||
private$flush()
|
||||
},
|
||||
|
||||
#' @description An internal method which shouldn't be used by others.
|
||||
#' Schedules `callback` for execution after some number of `millis`
|
||||
#' milliseconds.
|
||||
#' @param millis The number of milliseconds on which to schedule a callback
|
||||
#' @param callback The function to schedule.
|
||||
.scheduleTask = function(millis, callback) {
|
||||
id <- private$timer$schedule(millis, callback)
|
||||
|
||||
# Return a deregistration callback
|
||||
function() {
|
||||
invisible(private$timer$unschedule(id))
|
||||
}
|
||||
},
|
||||
|
||||
#' @description Simulate the passing of time by the given number of milliseconds.
|
||||
#' @param millis The number of milliseconds to advance time.
|
||||
elapse = function(millis) {
|
||||
msLeft <- millis
|
||||
|
||||
while (msLeft > 0){
|
||||
t <- private$timer$timeToNextEvent()
|
||||
|
||||
if (is.infinite(t) || t <= 0 || msLeft < t){
|
||||
# Either there's no good upcoming event or we can't make it to it in the allotted time.
|
||||
break
|
||||
}
|
||||
msLeft <- msLeft - t
|
||||
private$timer$elapse(t)
|
||||
|
||||
# timerCallbacks must run before flushReact.
|
||||
private$timer$executeElapsed()
|
||||
private$flush()
|
||||
}
|
||||
|
||||
private$timer$elapse(msLeft)
|
||||
|
||||
# Run again in case our callbacks resulted in a scheduled
|
||||
# function that needs executing.
|
||||
private$timer$executeElapsed()
|
||||
private$flush()
|
||||
},
|
||||
|
||||
#' @description An internal method which shouldn't be used by others.
|
||||
#' @return Elapsed time in milliseconds.
|
||||
.now = function() {
|
||||
private$timer$getElapsed()
|
||||
},
|
||||
|
||||
#' @description An internal method which shouldn't be used by others.
|
||||
#' Defines an output in a way that sets private$currentOutputName
|
||||
#' appropriately.
|
||||
#' @param name The name of the output.
|
||||
#' @param func The render definition.
|
||||
#' @param label Not used.
|
||||
defineOutput = function(name, func, label) {
|
||||
force(name)
|
||||
|
||||
if (!is.null(private$outs[[name]]$obs)) {
|
||||
private$outs[[name]]$obs$destroy()
|
||||
}
|
||||
|
||||
if (is.null(func)) func <- missingOutput
|
||||
|
||||
if (!is.function(func))
|
||||
stop(paste("Unexpected", class(func), "output for", name))
|
||||
|
||||
with_no_otel_collect({
|
||||
obs <- observe({
|
||||
# We could just stash the promise, but we get an "unhandled promise error". This bypasses
|
||||
prom <- NULL
|
||||
tryCatch({
|
||||
v <- private$withCurrentOutput(name, func(self, name))
|
||||
if (!is.promise(v)){
|
||||
# Make our sync value into a promise
|
||||
prom <- promise_resolve(v)
|
||||
} else {
|
||||
prom <- v
|
||||
}
|
||||
}, error=function(e){
|
||||
# Error running value()
|
||||
prom <<- promise_reject(e)
|
||||
})
|
||||
|
||||
private$outs[[name]]$promise <- hybrid_chain(
|
||||
prom,
|
||||
function(v){
|
||||
list(val = v, err = NULL)
|
||||
}, catch=function(e){
|
||||
if (
|
||||
!inherits(e, c("shiny.custom.error", "shiny.output.cancel", "shiny.output.progress", "shiny.silent.error"))
|
||||
) {
|
||||
self$unhandledError(e, close = FALSE)
|
||||
}
|
||||
list(val = NULL, err = e)
|
||||
})
|
||||
})
|
||||
})
|
||||
private$outs[[name]] <- list(obs = obs, func = func, promise = NULL)
|
||||
},
|
||||
|
||||
#' @description An internal method which shouldn't be used by others. Forces
|
||||
#' evaluation of any reactive dependencies of the output function.
|
||||
#' @param name The name of the output.
|
||||
#' @return The return value of the function responsible for rendering the
|
||||
#' output.
|
||||
getOutput = function(name) {
|
||||
# Unlike the real outputs, we're going to return the last value rather than the unevaluated function
|
||||
if (is.null(private$outs[[name]])) {
|
||||
stop("The test referenced an output that hasn't been defined yet: output$", name)
|
||||
}
|
||||
|
||||
if (is.null(private$outs[[name]]$promise)) {
|
||||
# Means the output was defined but the observer hasn't had a chance to run
|
||||
# yet. Run flushReact() now to force the observer to run.
|
||||
flushReact()
|
||||
|
||||
if (is.null(private$outs[[name]]$promise)) {
|
||||
stop("output$", name, " encountered an unexpected error resolving its promise")
|
||||
}
|
||||
}
|
||||
|
||||
# Make promise return
|
||||
v <- extract(private$outs[[name]]$promise)
|
||||
if (!is.null(v$err)){
|
||||
stop(v$err)
|
||||
} else if (private$file_generators$has(self$ns(name))) {
|
||||
download <- private$file_generators$get(self$ns(name))
|
||||
private$renderFile(self$ns(name), download)
|
||||
} else {
|
||||
v$val
|
||||
}
|
||||
},
|
||||
|
||||
#' @description Returns the given id prefixed by this namespace's id.
|
||||
#' @param id The id to prefix with a namespace id.
|
||||
#' @return The id with a namespace prefix.
|
||||
ns = function(id) {
|
||||
NS(private$nsPrefix, id)
|
||||
},
|
||||
#' @description Trigger a reactive flush right now.
|
||||
flushReact = function(){
|
||||
private$flush()
|
||||
},
|
||||
#' @description Create and return a namespace-specific session proxy.
|
||||
#' @param namespace Character vector indicating a namespace.
|
||||
#' @return A new session proxy.
|
||||
makeScope = function(namespace) {
|
||||
ns <- NS(namespace)
|
||||
createSessionProxy(
|
||||
self,
|
||||
input = .createReactiveValues(private$.input, readonly = TRUE, ns = ns),
|
||||
output = structure(.createOutputWriter(self, ns = ns), class = "shinyoutput"),
|
||||
makeScope = function(namespace) self$makeScope(ns(namespace)),
|
||||
ns = function(namespace) ns(namespace),
|
||||
setInputs = function(...) {
|
||||
self$setInputs(!!!mapNames(ns, rlang::dots_list(..., .homonyms = "error")))
|
||||
}
|
||||
)
|
||||
},
|
||||
#' @description Set the environment associated with a testServer() call, but
|
||||
#' only if it has not previously been set. This ensures that only the
|
||||
#' environment of the outermost module under test is the one retained. In
|
||||
#' other words, the first assignment wins.
|
||||
#' @param env The environment to retain.
|
||||
#' @return The provided `env`.
|
||||
setEnv = function(env) {
|
||||
if (is.null(self$env)) {
|
||||
stopifnot(all(c("input", "output", "session") %in% ls(env)))
|
||||
self$env <- env
|
||||
}
|
||||
},
|
||||
#' @description Set the value returned by the module call and proactively
|
||||
#' flush. Note that this method may be called multiple times if modules
|
||||
#' are nested. The last assignment, corresponding to an invocation of
|
||||
#' setReturned() in the outermost module, wins.
|
||||
#' @param value The value returned from the module
|
||||
#' @return The provided `value`.
|
||||
setReturned = function(value) {
|
||||
self$returned <- value
|
||||
value
|
||||
},
|
||||
#' @description Get the value returned by the module call.
|
||||
#' @return The value returned by the module call
|
||||
getReturned = function() self$returned,
|
||||
#' @description Generate a distinct character identifier for use as a proxy
|
||||
#' namespace.
|
||||
#' @return A character identifier unique to the current session.
|
||||
genId = function() {
|
||||
private$idCounter <- private$idCounter + 1
|
||||
paste0("proxy", private$idCounter)
|
||||
},
|
||||
#' @description Provides a way to access the root `MockShinySession` from
|
||||
#' any descendant proxy.
|
||||
#' @return The root `MockShinySession`.
|
||||
rootScope = function() {
|
||||
self
|
||||
},
|
||||
#' @description Add an unhandled error callback.
|
||||
#' @param callback The callback to add, which should accept an error object
|
||||
#' as its first argument.
|
||||
#' @return A deregistration function.
|
||||
onUnhandledError = function(callback) {
|
||||
private$unhandledErrorCallbacks$register(callback)
|
||||
},
|
||||
#' @description Called by observers when a reactive expression errors.
|
||||
#' @param e An error object.
|
||||
#' @param close If `TRUE`, the session will be closed after the error is
|
||||
#' handled, defaults to `FALSE`.
|
||||
unhandledError = function(e, close = TRUE) {
|
||||
if (close) {
|
||||
class(e) <- c("shiny.error.fatal", class(e))
|
||||
}
|
||||
|
||||
private$unhandledErrorCallbacks$invoke(e, onError = printError)
|
||||
.globals$onUnhandledErrorCallbacks$invoke(e, onError = printError)
|
||||
|
||||
if (close) self$close()
|
||||
},
|
||||
#' @description Freeze a value until the flush cycle completes.
|
||||
#' @param x A `ReactiveValues` object.
|
||||
#' @param name The name of a reactive value within `x`.
|
||||
freezeValue = function(x, name) {
|
||||
if (!is.reactivevalues(x))
|
||||
stop("x must be a reactivevalues object")
|
||||
|
||||
impl <- .subset2(x, 'impl')
|
||||
key <- .subset2(x, 'ns')(name)
|
||||
impl$freeze(key)
|
||||
self$onFlushed(function() impl$thaw(key))
|
||||
},
|
||||
#' @description Registers the given callback to be invoked when the session
|
||||
#' is closed (i.e. the connection to the client has been severed). The
|
||||
#' return value is a function which unregisters the callback. If multiple
|
||||
#' callbacks are registered, the order in which they are invoked is not
|
||||
#' guaranteed.
|
||||
#' @param sessionEndedCallback Function to call when the session ends.
|
||||
onSessionEnded = function(sessionEndedCallback) {
|
||||
self$onEnded(sessionEndedCallback)
|
||||
},
|
||||
#' @description Associated a downloadable file with the session.
|
||||
#' @param name The un-namespaced output name to associate with the
|
||||
#' downloadable file.
|
||||
#' @param filename A string or function designating the name of the file.
|
||||
#' @param contentType A string of the content type of the file. Not used by
|
||||
#' `MockShinySession`.
|
||||
#' @param content A function that takes a single argument file that is a
|
||||
#' file path (string) of a nonexistent temp file, and writes the content
|
||||
#' to that file path. (Reactive values and functions may be used from this
|
||||
#' function.)
|
||||
registerDownload = function(name, filename, contentType, content) {
|
||||
private$file_generators$set(self$ns(name), list(
|
||||
filename = if (is.function(filename)) filename else function() filename,
|
||||
content = content
|
||||
))
|
||||
},
|
||||
#' @description Get information about the output that is currently being
|
||||
#' executed.
|
||||
#' @return A list with with the `name` of the output. If no output is
|
||||
#' currently being executed, this will return `NULL`.
|
||||
getCurrentOutputInfo = function() {
|
||||
name <- private$currentOutputName
|
||||
if (is.null(name)) NULL else list(name = name)
|
||||
}
|
||||
),
|
||||
private = list(
|
||||
# @field .input Internal ReactiveValues object for normal input sent from client.
|
||||
.input = NULL,
|
||||
# @field flushCBs `Callbacks` called before flush.
|
||||
flushCBs = NULL,
|
||||
# @field flushedCBs `Callbacks` called after flush.
|
||||
flushedCBs = NULL,
|
||||
# @field endedCBs `Callbacks` called when session ends.
|
||||
endedCBs = NULL,
|
||||
# @field unhandledErrorCallbacks `Callbacks` called when an unhandled error
|
||||
# occurs.
|
||||
unhandledErrorCallbacks = Callbacks$new(),
|
||||
# @field timer `MockableTimerCallbacks` called at particular times.
|
||||
timer = NULL,
|
||||
# @field was_closed Set to `TRUE` once the session is closed.
|
||||
was_closed = FALSE,
|
||||
# @field outs List of namespaced output names.
|
||||
outs = list(),
|
||||
# @field nsPrefix Prefix with which to namespace inputs and outputs.
|
||||
nsPrefix = "mock-session",
|
||||
# @field idCounter Incremented every time `$genId()` is called.
|
||||
idCounter = 0,
|
||||
# @field file_generators Map of namespaced output names to lists with
|
||||
# `filename` and `output` elements, each a function. Updated by
|
||||
# `$registerDownload()` and read by `$getOutput()`. Files are generated
|
||||
# on demand when the output is accessed.
|
||||
file_generators = NULL,
|
||||
# @field currentOutputName Namespaced name of the currently executing
|
||||
#' output, or `NULL` if no output is currently executing.
|
||||
currentOutputName = NULL,
|
||||
|
||||
# @description Writes a downloadable file to disk. If the `content` function
|
||||
# associated with a download handler does not write a file, an error is
|
||||
# signaled. Created files are deleted upon session close.
|
||||
# @param name The eamespaced output name associated with the downloadable
|
||||
# file.
|
||||
# @param download List with two names, `filename` and `content`. Both should
|
||||
# be functions. `filename` should take no arguments and return a string.
|
||||
# `content` should accept a path argument and create a file at that path.
|
||||
# @return A path to a temp file.
|
||||
renderFile = function(name, download) {
|
||||
# We make our own tempdir here because it's not safe to delete the result
|
||||
# of tempdir().
|
||||
tmpd <- tempfile()
|
||||
dir.create(tmpd, recursive = TRUE)
|
||||
self$onSessionEnded(function() unlink(tmpd, recursive = TRUE))
|
||||
file <- file.path(tmpd, download$filename())
|
||||
download$content(file)
|
||||
if (!file.exists(file))
|
||||
error("downloadHandler for ", name, " did not write a file.")
|
||||
file
|
||||
},
|
||||
|
||||
# @description Calls `shiny:::flushReact()` and executes all callbacks
|
||||
# related to reactivity.
|
||||
flush = function(){
|
||||
isolate(private$flushCBs$invoke(..stacktraceon = TRUE))
|
||||
shiny:::flushReact() # namespace to avoid calling our own method
|
||||
isolate(private$flushedCBs$invoke(..stacktraceon = TRUE))
|
||||
later::run_now()
|
||||
},
|
||||
|
||||
# @description Produces a warning if the option `shiny.mocksession.warn` is
|
||||
# unset and not `FALSE`.
|
||||
# @param name The name of the mocked method.
|
||||
# @param msg A message describing why the method is not implemented.
|
||||
noopWarn = function(name, msg) {
|
||||
if (getOption("shiny.mocksession.warn", FALSE) == FALSE)
|
||||
return(invisible())
|
||||
out <- paste0(name, " is not fully implemented by MockShinySession: ", msg)
|
||||
out <- paste0(out, "\n", "To disable messages like this, run `options(shiny.mocksession.warn=FALSE)`")
|
||||
warning(out, call. = FALSE)
|
||||
},
|
||||
|
||||
# @description Binds a domain to `expr` and uses `createVarPromiseDomain()`
|
||||
# to ensure `private$currentOutputName` is set to `name` around any of
|
||||
# the promise's callbacks. Domains are something like dynamic scopes but
|
||||
# for promise chains instead of the call stack.
|
||||
# @return A promise.
|
||||
withCurrentOutput = function(name, expr) {
|
||||
if (!is.null(private$currentOutputName)) {
|
||||
stop("Nested calls to withCurrentOutput() are not allowed.")
|
||||
}
|
||||
|
||||
with_promise_domain(
|
||||
createVarPromiseDomain(private, "currentOutputName", name),
|
||||
expr
|
||||
)
|
||||
}
|
||||
),
|
||||
active = list(
|
||||
#' @field files For internal use only.
|
||||
files = function() stop("$files is for internal use only."),
|
||||
#' @field downloads For internal use only.
|
||||
downloads = function() stop("$downloads is for internal use only."),
|
||||
#' @field closed Deprecated in `ShinySession` and signals an error.
|
||||
closed = function() stop("$closed is deprecated"),
|
||||
#' @field session Deprecated in ShinySession and signals an error.
|
||||
session = function() stop("$session is deprecated"),
|
||||
#' @field request An empty environment where the request should be. The request isn't meaningfully mocked currently.
|
||||
request = function(value) {
|
||||
if (!missing(value)){
|
||||
stop("session$request can't be assigned to")
|
||||
}
|
||||
warning("session$request doesn't currently simulate a realistic request on MockShinySession")
|
||||
new.env(parent=emptyenv())
|
||||
}
|
||||
)
|
||||
)
|
||||
206
R/modal.R
Normal file
206
R/modal.R
Normal file
@@ -0,0 +1,206 @@
|
||||
#' Show or remove a modal dialog
|
||||
#'
|
||||
#' This causes a modal dialog to be displayed in the client browser, and is
|
||||
#' typically used with [modalDialog()].
|
||||
#'
|
||||
#' @param ui UI content to show in the modal.
|
||||
#' @param session The `session` object passed to function given to
|
||||
#' `shinyServer`.
|
||||
#'
|
||||
#' @seealso [modalDialog()] for examples.
|
||||
#' @export
|
||||
showModal <- function(ui, session = getDefaultReactiveDomain()) {
|
||||
res <- processDeps(ui, session)
|
||||
|
||||
session$sendModal("show",
|
||||
list(
|
||||
html = res$html,
|
||||
deps = res$deps
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#' @rdname showModal
|
||||
#' @export
|
||||
removeModal <- function(session = getDefaultReactiveDomain()) {
|
||||
session$sendModal("remove", NULL)
|
||||
}
|
||||
|
||||
|
||||
#' Create a modal dialog UI
|
||||
#'
|
||||
#' @description
|
||||
#' `modalDialog()` creates the UI for a modal dialog, using Bootstrap's modal
|
||||
#' class. Modals are typically used for showing important messages, or for
|
||||
#' presenting UI that requires input from the user, such as a user name and
|
||||
#' password input.
|
||||
#'
|
||||
#' `modalButton()` creates a button that will dismiss the dialog when clicked,
|
||||
#' typically used when customising the `footer`.
|
||||
#'
|
||||
#' @inheritParams actionButton
|
||||
#' @param ... UI elements for the body of the modal dialog box.
|
||||
#' @param title An optional title for the dialog.
|
||||
#' @param footer UI for footer. Use `NULL` for no footer.
|
||||
#' @param size One of `"s"` for small, `"m"` (the default) for medium,
|
||||
#' `"l"` for large, or `"xl"` for extra large. Note that `"xl"` only
|
||||
#' works with Bootstrap 4 and above (to opt-in to Bootstrap 4+,
|
||||
#' pass [bslib::bs_theme()] to the `theme` argument of a page container
|
||||
#' like [fluidPage()]).
|
||||
#' @param easyClose If `TRUE`, the modal dialog can be dismissed by
|
||||
#' clicking outside the dialog box, or be pressing the Escape key. If
|
||||
#' `FALSE` (the default), the modal dialog can't be dismissed in those
|
||||
#' ways; instead it must be dismissed by clicking on a `modalButton()`, or
|
||||
#' from a call to [removeModal()] on the server.
|
||||
#' @param fade If `FALSE`, the modal dialog will have no fade-in animation
|
||||
#' (it will simply appear rather than fade in to view).
|
||||
#'
|
||||
#' @examples
|
||||
#' if (interactive()) {
|
||||
#' # Display an important message that can be dismissed only by clicking the
|
||||
#' # dismiss button.
|
||||
#' shinyApp(
|
||||
#' ui = basicPage(
|
||||
#' actionButton("show", "Show modal dialog")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' observeEvent(input$show, {
|
||||
#' showModal(modalDialog(
|
||||
#' title = "Important message",
|
||||
#' "This is an important message!"
|
||||
#' ))
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # Display a message that can be dismissed by clicking outside the modal dialog,
|
||||
#' # or by pressing Esc.
|
||||
#' shinyApp(
|
||||
#' ui = basicPage(
|
||||
#' actionButton("show", "Show modal dialog")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' observeEvent(input$show, {
|
||||
#' showModal(modalDialog(
|
||||
#' title = "Somewhat important message",
|
||||
#' "This is a somewhat important message.",
|
||||
#' easyClose = TRUE,
|
||||
#' footer = NULL
|
||||
#' ))
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # Display a modal that requires valid input before continuing.
|
||||
#' shinyApp(
|
||||
#' ui = basicPage(
|
||||
#' actionButton("show", "Show modal dialog"),
|
||||
#' verbatimTextOutput("dataInfo")
|
||||
#' ),
|
||||
#'
|
||||
#' server = function(input, output) {
|
||||
#' # reactiveValues object for storing current data set.
|
||||
#' vals <- reactiveValues(data = NULL)
|
||||
#'
|
||||
#' # Return the UI for a modal dialog with data selection input. If 'failed' is
|
||||
#' # TRUE, then display a message that the previous value was invalid.
|
||||
#' dataModal <- function(failed = FALSE) {
|
||||
#' modalDialog(
|
||||
#' textInput("dataset", "Choose data set",
|
||||
#' placeholder = 'Try "mtcars" or "abc"'
|
||||
#' ),
|
||||
#' span('(Try the name of a valid data object like "mtcars", ',
|
||||
#' 'then a name of a non-existent object like "abc")'),
|
||||
#' if (failed)
|
||||
#' div(tags$b("Invalid name of data object", style = "color: red;")),
|
||||
#'
|
||||
#' footer = tagList(
|
||||
#' modalButton("Cancel"),
|
||||
#' actionButton("ok", "OK")
|
||||
#' )
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' # Show modal when button is clicked.
|
||||
#' observeEvent(input$show, {
|
||||
#' showModal(dataModal())
|
||||
#' })
|
||||
#'
|
||||
#' # When OK button is pressed, attempt to load the data set. If successful,
|
||||
#' # remove the modal. If not show another modal, but this time with a failure
|
||||
#' # message.
|
||||
#' observeEvent(input$ok, {
|
||||
#' # Check that data object exists and is data frame.
|
||||
#' if (!is.null(input$dataset) && nzchar(input$dataset) &&
|
||||
#' exists(input$dataset) && is.data.frame(get(input$dataset))) {
|
||||
#' vals$data <- get(input$dataset)
|
||||
#' removeModal()
|
||||
#' } else {
|
||||
#' showModal(dataModal(failed = TRUE))
|
||||
#' }
|
||||
#' })
|
||||
#'
|
||||
#' # Display information about selected data
|
||||
#' output$dataInfo <- renderPrint({
|
||||
#' if (is.null(vals$data))
|
||||
#' "No data selected"
|
||||
#' else
|
||||
#' summary(vals$data)
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#' @export
|
||||
modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"),
|
||||
size = c("m", "s", "l", "xl"), easyClose = FALSE, fade = TRUE) {
|
||||
|
||||
size <- match.arg(size)
|
||||
|
||||
backdrop <- if (!easyClose) "static"
|
||||
keyboard <- if (!easyClose) "false"
|
||||
div(
|
||||
id = "shiny-modal",
|
||||
class = "modal",
|
||||
class = if (fade) "fade",
|
||||
tabindex = "-1",
|
||||
`data-backdrop` = backdrop,
|
||||
`data-bs-backdrop` = backdrop,
|
||||
`data-keyboard` = keyboard,
|
||||
`data-bs-keyboard` = keyboard,
|
||||
|
||||
div(
|
||||
class = "modal-dialog",
|
||||
class = switch(size, s = "modal-sm", m = NULL, l = "modal-lg", xl = "modal-xl"),
|
||||
div(class = "modal-content",
|
||||
if (!is.null(title)) div(class = "modal-header",
|
||||
tags$h4(class = "modal-title", title)
|
||||
),
|
||||
div(class = "modal-body", ...),
|
||||
if (!is.null(footer)) div(class = "modal-footer", footer)
|
||||
)
|
||||
),
|
||||
# jQuery plugin doesn't work in Bootstrap 5, but vanilla JS doesn't work in Bootstrap 4 :sob:
|
||||
tags$script(HTML(
|
||||
"if (window.bootstrap && !window.bootstrap.Modal.VERSION.match(/^4\\./)) {
|
||||
var modal = new bootstrap.Modal(document.getElementById('shiny-modal'));
|
||||
modal.show();
|
||||
} else {
|
||||
$('#shiny-modal').modal().focus();
|
||||
}"
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
#' @export
|
||||
#' @rdname modalDialog
|
||||
modalButton <- function(label, icon = NULL) {
|
||||
tags$button(
|
||||
type = "button",
|
||||
class = "btn btn-default",
|
||||
`data-dismiss` = "modal",
|
||||
`data-bs-dismiss` = "modal",
|
||||
validateIcon(icon), label
|
||||
)
|
||||
}
|
||||
199
R/modules.R
Normal file
199
R/modules.R
Normal file
@@ -0,0 +1,199 @@
|
||||
# Creates an object whose $ and [[ pass through to the parent
|
||||
# session, unless the name is matched in ..., in which case
|
||||
# that value is returned instead. (See Decorator pattern.)
|
||||
createSessionProxy <- function(parentSession, ...) {
|
||||
e <- new.env(parent = emptyenv())
|
||||
e$parent <- parentSession
|
||||
e$overrides <- list(...)
|
||||
|
||||
structure(
|
||||
e,
|
||||
class = "session_proxy"
|
||||
)
|
||||
}
|
||||
|
||||
#' @export
|
||||
`$.session_proxy` <- function(x, name) {
|
||||
if (name %in% names(.subset2(x, "overrides")))
|
||||
.subset2(x, "overrides")[[name]]
|
||||
else
|
||||
.subset2(x, "parent")[[name]]
|
||||
}
|
||||
|
||||
#' @export
|
||||
`[[.session_proxy` <- `$.session_proxy`
|
||||
|
||||
|
||||
#' @export
|
||||
`$<-.session_proxy` <- function(x, name, value) {
|
||||
# this line allows users to write into session$userData
|
||||
# (e.g. it allows something like `session$userData$x <- TRUE`,
|
||||
# but not `session$userData <- TRUE`) from within a module
|
||||
# without any hacks (see PR #1732)
|
||||
if (identical(x[[name]], value)) return(x)
|
||||
|
||||
# Special case for $options (issue #3112)
|
||||
if (name == "options") {
|
||||
session <- find_ancestor_session(x)
|
||||
session[[name]] <- value
|
||||
return(x)
|
||||
}
|
||||
|
||||
stop("Attempted to assign value on session proxy.")
|
||||
}
|
||||
|
||||
`[[<-.session_proxy` <- `$<-.session_proxy`
|
||||
|
||||
# Given a session_proxy, search `parent` recursively to find the real
|
||||
# ShinySession object. If given a ShinySession, simply return it.
|
||||
find_ancestor_session <- function(x, depth = 20) {
|
||||
if (depth < 0) {
|
||||
stop("ShinySession not found")
|
||||
}
|
||||
if (inherits(x, "ShinySession")) {
|
||||
return(x)
|
||||
}
|
||||
if (inherits(x, "session_proxy")) {
|
||||
return(find_ancestor_session(.subset2(x, "parent"), depth-1))
|
||||
}
|
||||
|
||||
stop("ShinySession not found")
|
||||
}
|
||||
|
||||
|
||||
#' Shiny modules
|
||||
#'
|
||||
#' Shiny's module feature lets you break complicated UI and server logic into
|
||||
#' smaller, self-contained pieces. Compared to large monolithic Shiny apps,
|
||||
#' modules are easier to reuse and easier to reason about. See the article at
|
||||
#' <https://shiny.rstudio.com/articles/modules.html> to learn more.
|
||||
#'
|
||||
#' Starting in Shiny 1.5.0, we recommend using `moduleServer` instead of
|
||||
#' [`callModule()`], because the syntax is a little easier
|
||||
#' to understand, and modules created with `moduleServer` can be tested with
|
||||
#' [`testServer()`].
|
||||
#'
|
||||
#' @param module A Shiny module server function.
|
||||
#' @param id An ID string that corresponds with the ID used to call the module's
|
||||
#' UI function.
|
||||
#' @param session Session from which to make a child scope (the default should
|
||||
#' almost always be used).
|
||||
#'
|
||||
#' @return The return value, if any, from executing the module server function
|
||||
#' @seealso <https://shiny.rstudio.com/articles/modules.html>
|
||||
#'
|
||||
#' @examples
|
||||
#' # Define the UI for a module
|
||||
#' counterUI <- function(id, label = "Counter") {
|
||||
#' ns <- NS(id)
|
||||
#' tagList(
|
||||
#' actionButton(ns("button"), label = label),
|
||||
#' verbatimTextOutput(ns("out"))
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' # Define the server logic for a module
|
||||
#' counterServer <- function(id) {
|
||||
#' moduleServer(
|
||||
#' id,
|
||||
#' function(input, output, session) {
|
||||
#' count <- reactiveVal(0)
|
||||
#' observeEvent(input$button, {
|
||||
#' count(count() + 1)
|
||||
#' })
|
||||
#' output$out <- renderText({
|
||||
#' count()
|
||||
#' })
|
||||
#' count
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' # Use the module in an app
|
||||
#' ui <- fluidPage(
|
||||
#' counterUI("counter1", "Counter #1"),
|
||||
#' counterUI("counter2", "Counter #2")
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' counterServer("counter1")
|
||||
#' counterServer("counter2")
|
||||
#' }
|
||||
#' if (interactive()) {
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#'
|
||||
#'
|
||||
#' # If you want to pass extra parameters to the module's server logic, you can
|
||||
#' # add them to your function. In this case `prefix` is text that will be
|
||||
#' # printed before the count.
|
||||
#' counterServer2 <- function(id, prefix = NULL) {
|
||||
#' moduleServer(
|
||||
#' id,
|
||||
#' function(input, output, session) {
|
||||
#' count <- reactiveVal(0)
|
||||
#' observeEvent(input$button, {
|
||||
#' count(count() + 1)
|
||||
#' })
|
||||
#' output$out <- renderText({
|
||||
#' paste0(prefix, count())
|
||||
#' })
|
||||
#' count
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' counterUI("counter", "Counter"),
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' counterServer2("counter", "The current count is: ")
|
||||
#' }
|
||||
#' if (interactive()) {
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @export
|
||||
moduleServer <- function(id, module, session = getDefaultReactiveDomain()) {
|
||||
if (inherits(session, "MockShinySession")) {
|
||||
body(module) <- rlang::expr({
|
||||
session$setEnv(base::environment())
|
||||
!!body(module)
|
||||
})
|
||||
session$setReturned(callModule(module, id, session = session))
|
||||
} else {
|
||||
callModule(module, id, session = session)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#' Invoke a Shiny module
|
||||
#'
|
||||
#' Note: As of Shiny 1.5.0, we recommend using [`moduleServer()`] instead of
|
||||
#' [`callModule()`], because the syntax is a little easier
|
||||
#' to understand, and modules created with `moduleServer` can be tested with
|
||||
#' [`testServer()`].
|
||||
#'
|
||||
#' @param module A Shiny module server function
|
||||
#' @param id An ID string that corresponds with the ID used to call the module's
|
||||
#' UI function
|
||||
#' @param ... Additional parameters to pass to module server function
|
||||
#' @param session Session from which to make a child scope (the default should
|
||||
#' almost always be used)
|
||||
#'
|
||||
#' @return The return value, if any, from executing the module server function
|
||||
#' @export
|
||||
callModule <- function(module, id, ..., session = getDefaultReactiveDomain()) {
|
||||
if (!inherits(session, c("ShinySession", "session_proxy", "MockShinySession"))) {
|
||||
stop("session must be a ShinySession or session_proxy object.")
|
||||
}
|
||||
childScope <- session$makeScope(id)
|
||||
|
||||
withReactiveDomain(childScope, {
|
||||
if (!is.function(module)) {
|
||||
stop("module argument must be a function")
|
||||
}
|
||||
|
||||
module(childScope$input, childScope$output, childScope, ...)
|
||||
})
|
||||
}
|
||||
106
R/notifications.R
Normal file
106
R/notifications.R
Normal file
@@ -0,0 +1,106 @@
|
||||
#' Show or remove a notification
|
||||
#'
|
||||
#' These functions show and remove notifications in a Shiny application.
|
||||
#'
|
||||
#' @param ui Content of message.
|
||||
#' @param action Message content that represents an action. For example, this
|
||||
#' could be a link that the user can click on. This is separate from `ui`
|
||||
#' so customized layouts can handle the main notification content separately
|
||||
#' from action content.
|
||||
#' @param duration Number of seconds to display the message before it
|
||||
#' disappears. Use `NULL` to make the message not automatically
|
||||
#' disappear.
|
||||
#' @param closeButton If `TRUE`, display a button which will make the
|
||||
#' notification disappear when clicked. If `FALSE` do not display.
|
||||
#' @param id A unique identifier for the notification.
|
||||
#'
|
||||
#' `id` is optional for `showNotification()`: Shiny will automatically create
|
||||
#' one if needed. If you do supply it, Shiny will update an existing
|
||||
#' notification if it exists, otherwise it will create a new one.
|
||||
#'
|
||||
#' `id` is required for `removeNotification()`.
|
||||
#' @param type A string which controls the color of the notification. One of
|
||||
#' "default" (gray), "message" (blue), "warning" (yellow), or "error" (red).
|
||||
#' @param session Session object to send notification to.
|
||||
#'
|
||||
#' @return An ID for the notification.
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' # Show a message when button is clicked
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' actionButton("show", "Show")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' observeEvent(input$show, {
|
||||
#' showNotification("Message text",
|
||||
#' action = a(href = "javascript:location.reload();", "Reload page")
|
||||
#' )
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' # App with show and remove buttons
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' actionButton("show", "Show"),
|
||||
#' actionButton("remove", "Remove")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' # A queue of notification IDs
|
||||
#' ids <- character(0)
|
||||
#' # A counter
|
||||
#' n <- 0
|
||||
#'
|
||||
#' observeEvent(input$show, {
|
||||
#' # Save the ID for removal later
|
||||
#' id <- showNotification(paste("Message", n), duration = NULL)
|
||||
#' ids <<- c(ids, id)
|
||||
#' n <<- n + 1
|
||||
#' })
|
||||
#'
|
||||
#' observeEvent(input$remove, {
|
||||
#' if (length(ids) > 0)
|
||||
#' removeNotification(ids[1])
|
||||
#' ids <<- ids[-1]
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#' @export
|
||||
showNotification <- function(ui, action = NULL, duration = 5,
|
||||
closeButton = TRUE, id = NULL,
|
||||
type = c("default", "message", "warning", "error"),
|
||||
session = getDefaultReactiveDomain())
|
||||
{
|
||||
|
||||
if (is.null(id))
|
||||
id <- createUniqueId(8)
|
||||
|
||||
res <- processDeps(ui, session)
|
||||
actionRes <- processDeps(action, session)
|
||||
|
||||
session$sendNotification("show",
|
||||
list(
|
||||
html = res$html,
|
||||
action = actionRes$html,
|
||||
deps = c(res$deps, actionRes$deps),
|
||||
duration = if (!is.null(duration)) duration * 1000,
|
||||
closeButton = closeButton,
|
||||
id = id,
|
||||
type = match.arg(type)
|
||||
)
|
||||
)
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
#' @rdname showNotification
|
||||
#' @export
|
||||
removeNotification <- function(id, session = getDefaultReactiveDomain()) {
|
||||
force(id)
|
||||
session$sendNotification("remove", id)
|
||||
id
|
||||
}
|
||||
65
R/otel-attr-srcref.R
Normal file
65
R/otel-attr-srcref.R
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
|
||||
# Very similar to srcrefFromShinyCall(),
|
||||
# however, this works when the function does not have a srcref attr set
|
||||
otel_srcref_attributes <- function(srcref, fn_name = NULL) {
|
||||
if (is.function(srcref)) {
|
||||
srcref <- getSrcRefs(srcref)[[1]][[1]]
|
||||
}
|
||||
|
||||
if (is.null(srcref)) {
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
stopifnot(inherits(srcref, "srcref"))
|
||||
|
||||
# Semantic conventions for code: https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/
|
||||
#
|
||||
# Inspiration from https://github.com/r-lib/testthat/pull/2087/files#diff-92de3306849d93d6f7e76c5aaa1b0c037e2d716f72848f8a1c70536e0c8a1564R123-R124
|
||||
filename <- attr(srcref, "srcfile")$filename
|
||||
dropNulls(list(
|
||||
"code.function.name" = fn_name,
|
||||
# Location attrs
|
||||
"code.file.path" = filename,
|
||||
"code.line.number" = srcref[1],
|
||||
"code.column.number" = srcref[2],
|
||||
# Remove these deprecated location names once Logfire supports the preferred names
|
||||
# https://github.com/pydantic/logfire/issues/1559
|
||||
"code.filepath" = filename,
|
||||
"code.lineno" = srcref[1],
|
||||
"code.column" = srcref[2]
|
||||
))
|
||||
}
|
||||
|
||||
#' Get the srcref for the call at the specified stack level
|
||||
#'
|
||||
#' If you need to go farther back in the `sys.call()` stack, supply a larger
|
||||
#' negative number to `which_offset`. The default of 0 gets the immediate
|
||||
#' caller. `-1` would get the caller's caller, and so on.
|
||||
#' @param which_offset The stack level to get the call from. Defaults to -1 (the
|
||||
#' immediate caller).
|
||||
#' @return An srcref object, or NULL if none is found.
|
||||
#' @noRd
|
||||
get_call_srcref <- function(which_offset = 0) {
|
||||
# Go back one call to account for this function itself
|
||||
call <- sys.call(which_offset - 1)
|
||||
|
||||
srcref <- attr(call, "srcref", exact = TRUE)
|
||||
srcref
|
||||
}
|
||||
|
||||
|
||||
append_otel_srcref_attrs <- function(attrs, call_srcref, fn_name) {
|
||||
if (is.null(call_srcref)) {
|
||||
return(attrs)
|
||||
}
|
||||
|
||||
srcref_attrs <- otel_srcref_attributes(call_srcref, fn_name)
|
||||
if (is.null(srcref_attrs)) {
|
||||
return(attrs)
|
||||
}
|
||||
|
||||
attrs[names(srcref_attrs)] <- srcref_attrs
|
||||
|
||||
attrs
|
||||
}
|
||||
55
R/otel-collect.R
Normal file
55
R/otel-collect.R
Normal file
@@ -0,0 +1,55 @@
|
||||
otel_collect_choices <- c(
|
||||
"none",
|
||||
"session",
|
||||
"reactive_update",
|
||||
"reactivity",
|
||||
"all"
|
||||
)
|
||||
|
||||
# Check if the collect level is sufficient
|
||||
otel_collect_is_enabled <- function(
|
||||
impl_level,
|
||||
# Listen to option and fall back to the env var
|
||||
opt_collect_level = getOption("shiny.otel.collect", Sys.getenv("SHINY_OTEL_COLLECT", "all"))
|
||||
) {
|
||||
opt_collect_level <- as_otel_collect(opt_collect_level)
|
||||
|
||||
which(opt_collect_level == otel_collect_choices) >=
|
||||
which(impl_level == otel_collect_choices)
|
||||
}
|
||||
|
||||
# Check if tracing is enabled and if the collect level is sufficient
|
||||
has_otel_collect <- function(collect) {
|
||||
# Only check pkg author input iff loaded with pkgload
|
||||
if (IS_SHINY_LOCAL_PKG) {
|
||||
stopifnot(length(collect) == 1, any(collect == otel_collect_choices))
|
||||
}
|
||||
|
||||
otel_is_tracing_enabled() && otel_collect_is_enabled(collect)
|
||||
}
|
||||
|
||||
# Run expr with otel collection disabled
|
||||
with_no_otel_collect <- function(expr) {
|
||||
withOtelCollect("none", expr)
|
||||
}
|
||||
|
||||
|
||||
## -- Helpers -----------------------------------------------------
|
||||
|
||||
# shiny.otel.collect can be:
|
||||
# "none"; To do nothing / fully opt-out
|
||||
# "session" for session/start events
|
||||
# "reactive_update" (includes "session" features) and reactive_update spans
|
||||
# "reactivity" (includes "reactive_update" features) and spans for all reactive things
|
||||
# "all" - Anything that Shiny can do. (Currently equivalent to the "reactivity" level)
|
||||
|
||||
as_otel_collect <- function(collect = "all") {
|
||||
if (!is.character(collect)) {
|
||||
stop("`collect` must be a character vector.")
|
||||
}
|
||||
|
||||
# Match to collect enum
|
||||
collect <- match.arg(collect, otel_collect_choices, several.ok = FALSE)
|
||||
|
||||
return(collect)
|
||||
}
|
||||
194
R/otel-enable.R
Normal file
194
R/otel-enable.R
Normal file
@@ -0,0 +1,194 @@
|
||||
# # Approach
|
||||
# Use flags on the reactive object to indicate whether to record OpenTelemetry spans.
|
||||
#
|
||||
# Cadence:
|
||||
# * `$.isRecordingOtel` - Whether to record OpenTelemetry spans for this reactive object
|
||||
# * `$.otelLabel` - The label to use for the OpenTelemetry span
|
||||
# * `$.otelAttrs` - Additional attributes to add to the OpenTelemetry span
|
||||
|
||||
|
||||
#' Add OpenTelemetry for reactivity to an object
|
||||
#'
|
||||
#' @description
|
||||
#'
|
||||
#' `enable_otel_*()` methods add OpenTelemetry flags for [reactive()] expressions
|
||||
#' and `render*` functions (like [renderText()], [renderTable()], ...).
|
||||
#'
|
||||
#' Wrapper to creating an active reactive OpenTelemetry span that closes when
|
||||
#' the reactive expression is done computing. Typically this is when the
|
||||
#' reactive expression finishes (synchronous) or when the returned promise is
|
||||
#' done computing (asynchronous).
|
||||
|
||||
#' @section Async with OpenTelemetry:
|
||||
#'
|
||||
#' With a reactive expression, the key and/or value expression can be
|
||||
#' _asynchronous_. In other words, they can be promises --- not regular R
|
||||
#' promises, but rather objects provided by the
|
||||
#' \href{https://rstudio.github.io/promises/}{\pkg{promises}} package, which
|
||||
#' are similar to promises in JavaScript. (See [promises::promise()] for more
|
||||
#' information.) You can also use [mirai::mirai()] or [future::future()]
|
||||
#' objects to run code in a separate process or even on a remote machine.
|
||||
#'
|
||||
#' When reactive expressions are being calculated in parallel (by having
|
||||
#' another reactive promise compute in the main process), the currently active
|
||||
#' OpenTelemetry span will be dynamically swapped out according to the
|
||||
#' currently active reactive expression. This means that as long as a promise
|
||||
#' was `then()`ed or `catch()`ed with an active OpenTelemetry span, the span
|
||||
#' will be correctly propagated to the next step (and subsequently other
|
||||
#' steps) in the promise chain.
|
||||
#'
|
||||
#' While the common case is for a reactive expression to be created
|
||||
#' synchronously, troubles arise when the reactive expression is created
|
||||
#' asynchronously. The span **must** be created before the reactive expression
|
||||
#' is executed, it **must** be active for the duration of the expression, and
|
||||
#' it **must** not be closed until the reactive expression is done executing.
|
||||
#' This is not easily achieved with a single function call, so we provide a
|
||||
#' way to create a reactive expression that is bound to an OpenTelemetry
|
||||
#' span.
|
||||
#'
|
||||
#' @section Span management and performance:
|
||||
#'
|
||||
#' Dev note - Barret 2025-10:
|
||||
#' Typically, an OpenTelemetry span (`otel_span`) will inherit from the parent
|
||||
#' span. This works well and we can think of the hierarchy as a tree. With
|
||||
#' `options("shiny.otel.collect" = <value>)`, we are able to control with a sliding
|
||||
#' dial how much of the tree we are interested in: "none", "session",
|
||||
#' "reactive_update", "reactivity", and finally "all".
|
||||
#'
|
||||
#' Leveraging this hierarchy, we can avoid creating spans that are not needed.
|
||||
#' The act of making a noop span takes on the order of 10microsec. Handling of
|
||||
#' the opspan is also in the 10s of microsecond range. We should avoid this when
|
||||
#' we **know** that we're not interested in the span. Therefore, manually
|
||||
#' handling spans should be considered for Shiny.
|
||||
#'
|
||||
#' * Q:
|
||||
#' * But what about app author who want the current span? Is there any
|
||||
#' guarantee that the current span is expected `reactive()` span?
|
||||
#' * A:
|
||||
#' * No. The current span is whatever the current span is. If the app author
|
||||
#' wants a specific span, they should create it themselves.
|
||||
#' * Proof:
|
||||
#' ```r
|
||||
#' noop <- otel::get_active_span()
|
||||
#' noop$get_context()$get_span_id()
|
||||
#' #> [1] "0000000000000000"
|
||||
#' ignore <- otelsdk::with_otel_record({
|
||||
#' a <- otel::start_local_active_span("a")
|
||||
#' a$get_context()$get_span_id() |> str()
|
||||
#' otel::with_active_span(noop, {
|
||||
#' otel::get_active_span()$get_context()$get_span_id() |> str()
|
||||
#' })
|
||||
#' })
|
||||
#' #> chr "2645e95715841e75"
|
||||
#' #> chr "2645e95715841e75"
|
||||
#' # ## It is reasonable to expect the second id to be `0000000000000000`, but it's not.
|
||||
#' ```
|
||||
#' Therefore, the app author has no guarantee that the current span is the
|
||||
#' span they're expecting. If the app author wants a specific span, they should
|
||||
#' create it themselves and let natural inheritance take over.
|
||||
#'
|
||||
#' Given this, I will imagine that app authors will set
|
||||
#' `options("shiny.otel.collect" = "reactive_update")` as their default behavior.
|
||||
#' Enough to know things are happening, but not overwhelming from **everything**
|
||||
#' that is reactive.
|
||||
#'
|
||||
#' To _light up_ a specific area, users can call `withr::with_options(list("shiny.otel.collect" = "all"), { ... })`.
|
||||
#'
|
||||
#' @param x The object to add caching to.
|
||||
#' @param ... Future parameter expansion.
|
||||
#' @noRd
|
||||
NULL
|
||||
|
||||
|
||||
enable_otel_reactive_val <- function(x) {
|
||||
|
||||
impl <- attr(x, ".impl", exact = TRUE)
|
||||
# Set flag for otel logging when setting the value
|
||||
impl$.isRecordingOtel <- TRUE
|
||||
|
||||
class(x) <- c("reactiveVal.otel", class(x))
|
||||
|
||||
x
|
||||
}
|
||||
|
||||
enable_otel_reactive_values <- function(x) {
|
||||
|
||||
impl <- .subset2(x, "impl")
|
||||
# Set flag for otel logging when setting values
|
||||
impl$.isRecordingOtel <- TRUE
|
||||
|
||||
class(x) <- c("reactivevalues.otel", class(x))
|
||||
|
||||
x
|
||||
}
|
||||
|
||||
enable_otel_reactive_expr <- function(x) {
|
||||
|
||||
domain <- reactive_get_domain(x)
|
||||
|
||||
impl <- attr(x, "observable", exact = TRUE)
|
||||
impl$.isRecordingOtel <- TRUE
|
||||
# Covers both reactive and reactive.event
|
||||
impl$.otelLabel <- otel_span_label_reactive(x, domain = impl$.domain)
|
||||
|
||||
class(x) <- c("reactiveExpr.otel", class(x))
|
||||
|
||||
x
|
||||
}
|
||||
|
||||
enable_otel_observe <- function(x) {
|
||||
x$.isRecordingOtel <- TRUE
|
||||
x$.otelLabel <- otel_span_label_observer(x, domain = x$.domain)
|
||||
|
||||
class(x) <- c("Observer.otel", class(x))
|
||||
invisible(x)
|
||||
}
|
||||
|
||||
|
||||
|
||||
enable_otel_shiny_render_function <- function(x) {
|
||||
|
||||
valueFunc <- force(x)
|
||||
otel_span_label <- NULL
|
||||
otel_span_attrs <- NULL
|
||||
|
||||
renderFunc <- function(...) {
|
||||
# Dynamically determine the span label given the current reactive domain
|
||||
if (is.null(otel_span_label)) {
|
||||
domain <- getDefaultReactiveDomain()
|
||||
otel_span_label <<-
|
||||
otel_span_label_render_function(x, domain = domain)
|
||||
otel_span_attrs <<- c(
|
||||
attr(x, "otelAttrs"),
|
||||
otel_session_id_attrs(domain)
|
||||
)
|
||||
}
|
||||
|
||||
with_otel_span(
|
||||
otel_span_label,
|
||||
{
|
||||
hybrid_then(
|
||||
valueFunc(...),
|
||||
on_failure = set_otel_exception_status_and_throw,
|
||||
# Must save the error object
|
||||
tee = FALSE
|
||||
)
|
||||
},
|
||||
attributes = otel_span_attrs
|
||||
)
|
||||
}
|
||||
|
||||
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
|
||||
class(renderFunc) <- c("shiny.render.function.otel", class(valueFunc))
|
||||
renderFunc
|
||||
}
|
||||
|
||||
|
||||
# ## If we ever expose a S3 function, I'd like to add this method.
|
||||
# bindOtel.function <- function(x, ...) {
|
||||
# cli::cli_abort(paste0(
|
||||
# "Don't know how to add OpenTelemetry recording to a plain function. ",
|
||||
# "If this is a {.code render*()} function for Shiny, it may need to be updated. ",
|
||||
# "Please see {.help shiny::bindOtel} for more information."
|
||||
# ))
|
||||
# }
|
||||
56
R/otel-error.R
Normal file
56
R/otel-error.R
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
has_seen_otel_exception <- function(cnd) {
|
||||
!is.null(cnd$.shiny_otel_exception)
|
||||
}
|
||||
|
||||
mark_otel_exception_as_seen <- function(cnd) {
|
||||
cnd$.shiny_otel_exception <- TRUE
|
||||
cnd
|
||||
}
|
||||
|
||||
set_otel_exception_status_and_throw <- function(cnd) {
|
||||
cnd <- set_otel_exception_status(cnd)
|
||||
|
||||
# Rethrow the (possibly updated) error
|
||||
signalCondition(cnd)
|
||||
}
|
||||
|
||||
set_otel_exception_status <- function(cnd) {
|
||||
if (inherits(cnd, "shiny.custom.error")) {
|
||||
# No-op
|
||||
} else if (inherits(cnd, "shiny.output.cancel")) {
|
||||
# No-op
|
||||
} else if (inherits(cnd, "shiny.output.progress")) {
|
||||
# No-op
|
||||
} else if (cnd_inherits(cnd, "shiny.silent.error")) {
|
||||
# No-op
|
||||
} else {
|
||||
# Only when an unknown error occurs do we set the span status to error
|
||||
span <- otel::get_active_span()
|
||||
|
||||
# Only record the exception once at the original point of failure,
|
||||
# not every reactive expression that it passes through
|
||||
if (!has_seen_otel_exception(cnd)) {
|
||||
span$record_exception(
|
||||
# Record a sanitized error if sanitization is enabled
|
||||
get_otel_error_obj(cnd)
|
||||
)
|
||||
cnd <- mark_otel_exception_as_seen(cnd)
|
||||
}
|
||||
|
||||
# Record the error status on the span for any context touching this error
|
||||
span$set_status("error")
|
||||
}
|
||||
|
||||
cnd
|
||||
}
|
||||
|
||||
|
||||
get_otel_error_obj <- function(e) {
|
||||
# Do not expose errors to otel if sanitization is enabled
|
||||
if (getOption("shiny.otel.sanitize.errors", TRUE)) {
|
||||
sanitized_error()
|
||||
} else {
|
||||
e
|
||||
}
|
||||
}
|
||||
198
R/otel-label.R
Normal file
198
R/otel-label.R
Normal file
@@ -0,0 +1,198 @@
|
||||
# observe mymod:<anonymous>
|
||||
# observe <anonymous>
|
||||
# observe mylabel
|
||||
|
||||
# -- Reactives --------------------------------------------------------------
|
||||
|
||||
#' OpenTelemetry Label Generation Functions
|
||||
#'
|
||||
#' Functions for generating formatted labels for OpenTelemetry tracing spans
|
||||
#' in Shiny applications. These functions handle module namespacing and
|
||||
#' cache/event modifiers for different Shiny reactive constructs.
|
||||
#'
|
||||
#' @param x The object to generate a label for (reactive, observer, etc.)
|
||||
#' @param label Character string label for reactive values
|
||||
#' @param key Character string key for reactiveValues operations
|
||||
#' @param ... Additional arguments (unused)
|
||||
#' @param domain Shiny domain object containing namespace information
|
||||
#'
|
||||
#' @return Character string formatted for OpenTelemetry span labels
|
||||
#' @name otel_label
|
||||
#' @noRd
|
||||
NULL
|
||||
|
||||
otel_span_label_reactive <- function(x, ..., domain) {
|
||||
fn_name <- otel_label_with_modifiers(
|
||||
x,
|
||||
"reactive",
|
||||
cache_class = "reactive.cache",
|
||||
event_class = "reactive.event"
|
||||
)
|
||||
|
||||
label <- attr(x, "observable", exact = TRUE)[[".label"]]
|
||||
otel_span_label <- otel_label_upgrade(label, domain = domain)
|
||||
|
||||
sprintf("%s %s", fn_name, otel_span_label)
|
||||
}
|
||||
|
||||
otel_span_label_render_function <- function(x, ..., domain) {
|
||||
fn_name <- otel_label_with_modifiers(
|
||||
x,
|
||||
"output",
|
||||
cache_class = "shiny.render.function.cache",
|
||||
event_class = "shiny.render.function.event"
|
||||
)
|
||||
|
||||
label <- getCurrentOutputInfo(session = domain)$name %||% "<unknown>"
|
||||
otel_span_label <- otel_label_upgrade(label, domain = domain)
|
||||
|
||||
sprintf("%s %s", fn_name, otel_span_label)
|
||||
}
|
||||
|
||||
otel_span_label_observer <- function(x, ..., domain) {
|
||||
fn_name <- otel_label_with_modifiers(
|
||||
x,
|
||||
"observe",
|
||||
cache_class = NULL, # Do not match a cache class here
|
||||
event_class = "Observer.event"
|
||||
)
|
||||
|
||||
otel_span_label <- otel_label_upgrade(x$.label, domain = domain)
|
||||
|
||||
sprintf("%s %s", fn_name, otel_span_label)
|
||||
}
|
||||
|
||||
# -- Set reactive value(s) ----------------------------------------------------
|
||||
|
||||
otel_log_label_set_reactive_val <- function(label, ..., domain) {
|
||||
sprintf(
|
||||
"Set reactiveVal %s",
|
||||
otel_label_upgrade(label, domain = domain)
|
||||
)
|
||||
}
|
||||
|
||||
otel_log_label_set_reactive_values <- function(label, key, ..., domain) {
|
||||
sprintf(
|
||||
"Set reactiveValues %s$%s",
|
||||
otel_label_upgrade(label, domain = domain),
|
||||
key
|
||||
)
|
||||
}
|
||||
|
||||
# -- ExtendedTask -------------------------------------------------------------
|
||||
|
||||
otel_span_label_extended_task <- function(label, suffix = NULL, ..., domain) {
|
||||
sprintf(
|
||||
"ExtendedTask %s",
|
||||
otel_label_upgrade(label, domain = domain)
|
||||
)
|
||||
}
|
||||
otel_log_label_extended_task_add_to_queue <- function(label, ..., domain) {
|
||||
sprintf(
|
||||
"ExtendedTask %s add to queue",
|
||||
otel_label_upgrade(label, domain = domain)
|
||||
)
|
||||
}
|
||||
|
||||
# -- Debounce / Throttle -------------------------------------------------------
|
||||
|
||||
otel_label_debounce <- function(label, ..., domain) {
|
||||
sprintf(
|
||||
"debounce %s",
|
||||
otel_label_upgrade(label, domain = domain)
|
||||
)
|
||||
}
|
||||
|
||||
otel_label_throttle <- function(label, ..., domain) {
|
||||
sprintf(
|
||||
"throttle %s",
|
||||
otel_label_upgrade(label, domain = domain)
|
||||
)
|
||||
}
|
||||
|
||||
# ---- Reactive Poll / File Reader -----------------------------------------------
|
||||
otel_label_reactive_poll <- function(label, ..., domain) {
|
||||
sprintf(
|
||||
"reactivePoll %s",
|
||||
otel_label_upgrade(label, domain = domain)
|
||||
)
|
||||
}
|
||||
otel_label_reactive_file_reader <- function(label, ..., domain) {
|
||||
sprintf(
|
||||
"reactiveFileReader %s",
|
||||
otel_label_upgrade(label, domain = domain)
|
||||
)
|
||||
}
|
||||
|
||||
# -- Helpers --------------------------------------------------------------
|
||||
|
||||
#' Modify function name based on object class modifiers
|
||||
#'
|
||||
#' @param x Object to check class of
|
||||
#' @param fn_name Base function name
|
||||
#' @param cache_class Optional class name that indicates cache operation
|
||||
#' @param event_class Optional class name that indicates event operation
|
||||
#'
|
||||
#' @return Modified function name with "cache" or "event" suffix if applicable
|
||||
#' @noRd
|
||||
otel_label_with_modifiers <- function(
|
||||
x,
|
||||
fn_name,
|
||||
cache_class = NULL,
|
||||
event_class = NULL
|
||||
) {
|
||||
for (x_class in rev(class(x))) {
|
||||
if (!is.null(cache_class) && x_class == cache_class) {
|
||||
fn_name <- sprintf("%s cache", fn_name)
|
||||
} else if (!is.null(event_class) && x_class == event_class) {
|
||||
fn_name <- sprintf("%s event", fn_name)
|
||||
}
|
||||
}
|
||||
|
||||
fn_name
|
||||
}
|
||||
|
||||
|
||||
#' Upgrade and format OpenTelemetry labels with module namespacing
|
||||
#'
|
||||
#' Processes labels for OpenTelemetry tracing, replacing default verbose labels
|
||||
#' with cleaner alternatives and prepending module namespaces when available.
|
||||
#'
|
||||
#' @param label Character string label to upgrade
|
||||
#' @param ... Additional arguments (unused)
|
||||
#' @param domain Shiny domain object containing namespace information
|
||||
#'
|
||||
#' @return Modified label string with module prefix if applicable
|
||||
#' @noRd
|
||||
#'
|
||||
#' @details
|
||||
#' Module prefix examples:
|
||||
#' - "" -> ""
|
||||
#' - "my-nested-mod-" -> "my-nested-mod"
|
||||
otel_label_upgrade <- function(label, ..., domain) {
|
||||
# By default, `observe()` sets the label to `observe(CODE)`
|
||||
# This label is too big and inconsistent.
|
||||
# Replace it with `<anonymous>`
|
||||
# (Similar with `eventReactive()` and `observeEvent()`)
|
||||
if (is_default_label(label) && grepl("(", label, fixed = TRUE)) {
|
||||
label <- "<anonymous>"
|
||||
# label <- sprintf("<anonymous> - %s", label)
|
||||
}
|
||||
|
||||
if (is.null(domain)) {
|
||||
return(label)
|
||||
}
|
||||
|
||||
namespace <- domain$ns("")
|
||||
|
||||
if (!nzchar(namespace)) {
|
||||
return(label)
|
||||
}
|
||||
|
||||
# Remove trailing module separator
|
||||
mod_ns <- sub(sprintf("%s$", ns.sep), "", namespace)
|
||||
|
||||
# Prepend the module name to the label
|
||||
# Ex: `"mymod:x"`
|
||||
sprintf("%s:%s", mod_ns, label)
|
||||
}
|
||||
114
R/otel-reactive-update.R
Normal file
114
R/otel-reactive-update.R
Normal file
@@ -0,0 +1,114 @@
|
||||
# * `session$userData[["_otel_span_reactive_update"]]` - The active reactive update span (or `NULL`)
|
||||
|
||||
|
||||
#' Start a `reactive_update` OpenTelemetry span and store it
|
||||
#'
|
||||
#' Used when a reactive expression is updated
|
||||
#' Will only start the span iff the otel tracing is enabled
|
||||
#' @param ... Ignored
|
||||
#' @param domain The reactive domain to associate with the span
|
||||
#' @return Invisibly returns.
|
||||
#' @seealso `otel_span_reactive_update_teardown()`
|
||||
#' @noRd
|
||||
otel_span_reactive_update_init <- function(..., domain) {
|
||||
|
||||
if (!has_otel_collect("reactive_update")) return()
|
||||
|
||||
# Ensure cleanup is registered only once per session
|
||||
if (is.null(domain$userData[["_otel_has_reactive_cleanup"]])) {
|
||||
domain$userData[["_otel_has_reactive_cleanup"]] <- TRUE
|
||||
|
||||
# Clean up any dangling reactive spans on an unplanned exit
|
||||
domain$onSessionEnded(function() {
|
||||
otel_span_reactive_update_teardown(domain = domain)
|
||||
})
|
||||
}
|
||||
|
||||
# Safety check
|
||||
if (is_otel_span(domain$userData[["_otel_span_reactive_update"]])) {
|
||||
stop("Reactive update span already exists")
|
||||
}
|
||||
|
||||
domain$userData[["_otel_span_reactive_update"]] <-
|
||||
start_otel_span(
|
||||
"reactive_update",
|
||||
...,
|
||||
attributes = otel_session_id_attrs(domain)
|
||||
)
|
||||
|
||||
invisible()
|
||||
}
|
||||
|
||||
#' End a `reactive_update` OpenTelemetry span and remove it from the session
|
||||
#' @param ... Ignored
|
||||
#' @param domain The reactive domain to associate with the span
|
||||
#' @return Invisibly returns.
|
||||
#' @seealso `otel_span_reactive_update_init()`
|
||||
#' @noRd
|
||||
otel_span_reactive_update_teardown <- function(..., domain) {
|
||||
ospan <- domain$userData[["_otel_span_reactive_update"]]
|
||||
|
||||
if (is_otel_span(ospan)) {
|
||||
otel::end_span(ospan)
|
||||
domain$userData[["_otel_span_reactive_update"]] <- NULL
|
||||
}
|
||||
|
||||
invisible()
|
||||
}
|
||||
|
||||
|
||||
#' Run expr within a `reactive_update` OpenTelemetry span
|
||||
#'
|
||||
#' Used to wrap the execution of a reactive expression. Will only
|
||||
#' require/activate the span iff the otel tracing is enabled
|
||||
#' @param expr The expression to executed within the span
|
||||
#' @param ... Ignored
|
||||
#' @param domain The reactive domain to associate with the span
|
||||
#' @noRd
|
||||
with_otel_span_reactive_update <- function(expr, ..., domain) {
|
||||
ospan <- domain$userData[["_otel_span_reactive_update"]]
|
||||
|
||||
if (!is_otel_span(ospan)) {
|
||||
return(force(expr))
|
||||
}
|
||||
|
||||
# Given the reactive update span is started before and ended when exec count
|
||||
# is 0, we only need to wrap the expr in the span context
|
||||
otel::with_active_span(ospan, {force(expr)})
|
||||
}
|
||||
|
||||
|
||||
#' Run expr within `reactive_update` otel span if not already active
|
||||
#'
|
||||
#' If the reactive update otel span is not already active, run the expression
|
||||
#' within the reactive update otel span context. This ensures that nested calls
|
||||
#' to reactive expressions do not attempt to re-enter the same span.
|
||||
#'
|
||||
#' This method is used within Context `run()` and running an Output's observer
|
||||
#' implementation
|
||||
#' @param expr The expression to executed within the span
|
||||
#' @param ... Ignored
|
||||
#' @param domain The reactive domain to associate with the span
|
||||
#' @noRd
|
||||
maybe_with_otel_span_reactive_update <- function(expr, ..., domain) {
|
||||
if (is.null(domain$userData[["_otel_reactive_update_is_active"]])) {
|
||||
domain$userData[["_otel_reactive_update_is_active"]] <- TRUE
|
||||
|
||||
# When the expression is done promising, clear the active flag
|
||||
hybrid_then(
|
||||
{
|
||||
with_otel_span_reactive_update(domain = domain, expr)
|
||||
},
|
||||
on_success = function(value) {
|
||||
domain$userData[["_otel_reactive_update_is_active"]] <- NULL
|
||||
},
|
||||
on_failure = function(e) {
|
||||
domain$userData[["_otel_reactive_update_is_active"]] <- NULL
|
||||
},
|
||||
# Return the value before the callbacks
|
||||
tee = TRUE
|
||||
)
|
||||
} else {
|
||||
expr
|
||||
}
|
||||
}
|
||||
96
R/otel-session.R
Normal file
96
R/otel-session.R
Normal file
@@ -0,0 +1,96 @@
|
||||
# Semantic conventions for session: https://opentelemetry.io/docs/specs/semconv/general/session/
|
||||
|
||||
#' Create and use session span and events
|
||||
#'
|
||||
#' If otel is disabled, the session span and events will not be created,
|
||||
#' however the expression will still be evaluated.
|
||||
#'
|
||||
#' Span: `session_start`, `session_end`
|
||||
#' @param expr Expression to evaluate within the session span
|
||||
#' @param ... Ignored
|
||||
#' @param domain The reactive domain
|
||||
#' @noRd
|
||||
otel_span_session_start <- function(expr, ..., domain) {
|
||||
|
||||
if (!has_otel_collect("session")) {
|
||||
return(force(expr))
|
||||
}
|
||||
|
||||
# Wrap the server initialization
|
||||
with_otel_span(
|
||||
"session_start",
|
||||
expr,
|
||||
attributes = otel::as_attributes(c(
|
||||
otel_session_id_attrs(domain),
|
||||
otel_session_attrs(domain)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
otel_span_session_end <- function(expr, ..., domain) {
|
||||
if (!has_otel_collect("session")) {
|
||||
return(force(expr))
|
||||
}
|
||||
|
||||
id_attrs <- otel_session_id_attrs(domain)
|
||||
with_otel_span(
|
||||
"session_end",
|
||||
expr,
|
||||
attributes = id_attrs
|
||||
)
|
||||
}
|
||||
|
||||
# -- Helpers -------------------------------
|
||||
|
||||
|
||||
# Occurs when the websocket connection is established
|
||||
otel_session_attrs <- function(domain) {
|
||||
# TODO: Future: Posit Connect integration
|
||||
# > we are still trying to identify all of the information we want to track/expose
|
||||
#
|
||||
# * `POSIT_PRODUCT` (Fallback to RSTUDIO_PRODUCT) for host environment
|
||||
# * `CONNECT_SERVER` envvar to get the `session.address`.
|
||||
# * `CONNECT_CONTENT_GUID` for the consistent app distinguisher
|
||||
# * Maybe `CONNECT_CONTENT_JOB_KEY`?
|
||||
# * Maybe `user.id` to be their user name: https://opentelemetry.io/docs/specs/semconv/registry/attributes/user/
|
||||
attrs <- list(
|
||||
server.path =
|
||||
sub(
|
||||
"/websocket/$", "/",
|
||||
domain[["request"]][["PATH_INFO"]] %||% ""
|
||||
),
|
||||
server.address = domain[["request"]][["HTTP_HOST"]] %||% "",
|
||||
server.origin = domain[["request"]][["HTTP_ORIGIN"]] %||% "",
|
||||
## Currently, Shiny does not expose QUERY_STRING when connecting the websocket
|
||||
# so we do not provide it here.
|
||||
# QUERY_STRING = domain[["request"]][["QUERY_STRING"]] %||% "",
|
||||
server.port = domain[["request"]][["SERVER_PORT"]] %||% NA_integer_
|
||||
)
|
||||
# Safely convert SERVER_PORT to integer
|
||||
# If conversion fails, leave as-is (string or empty)
|
||||
# This avoids warnings/errors if SERVER_PORT is not a valid integer
|
||||
server_port <- suppressWarnings(as.integer(attrs$server.port))
|
||||
if (!is.na(server_port)) {
|
||||
attrs$server.port <- server_port
|
||||
}
|
||||
|
||||
attrs
|
||||
}
|
||||
|
||||
otel_session_id_attrs <- function(domain) {
|
||||
token <- domain$token
|
||||
if (is.null(token)) {
|
||||
return(list())
|
||||
}
|
||||
|
||||
list(
|
||||
# Convention for client-side with session.start and session.end events
|
||||
# https://opentelemetry.io/docs/specs/semconv/general/session/
|
||||
#
|
||||
# Since we are the server, we'll add them as an attribute to _every_ span
|
||||
# within the session as we don't know exactly when they will be called.
|
||||
# Given it's only a single attribute, the cost should be minimal, but it ties every reactive calculation together.
|
||||
session.id = token
|
||||
)
|
||||
}
|
||||
127
R/otel-shiny.R
Normal file
127
R/otel-shiny.R
Normal file
@@ -0,0 +1,127 @@
|
||||
# Used by otel to identify the tracer and logger for this package
|
||||
# https://github.com/r-lib/otel/blob/afc31bc1f4bd177870d44b051ada1d9e4e685346/R/tracer-name.R#L33-L49
|
||||
# DO NOT CHANGE THIS VALUE without understanding the implications for existing telemetry data!
|
||||
otel_tracer_name <- "co.posit.r-package.shiny"
|
||||
|
||||
init_otel <- function() {
|
||||
.globals$otel_tracer <- otel::get_tracer()
|
||||
.globals$otel_is_tracing_enabled <- otel::is_tracing_enabled(.globals$otel_tracer)
|
||||
|
||||
.globals$otel_logger <- otel::get_logger()
|
||||
# .globals$otel_is_logging_enabled <- otel::is_logging_enabled()
|
||||
}
|
||||
on_load({init_otel()})
|
||||
|
||||
#' Run expr within a Shiny OpenTelemetry recording context
|
||||
#'
|
||||
#' Reset the OpenTelemetry tracer and logger for Shiny.
|
||||
#' Used for testing purposes only.
|
||||
#' @param expr Expression to evaluate within the recording context
|
||||
#' @return The result of evaluating `otelsdk::with_otel_record(expr)` with freshly enabled Shiny otel tracer and logger
|
||||
#' @noRd
|
||||
with_shiny_otel_record <- function(expr) {
|
||||
# Only use within internal testthat tests
|
||||
stopifnot(testthat__is_testing())
|
||||
withr::defer({ init_otel() })
|
||||
|
||||
otelsdk::with_otel_record({
|
||||
init_otel()
|
||||
|
||||
force(expr)
|
||||
})
|
||||
}
|
||||
|
||||
#' Check if OpenTelemetry tracing is enabled
|
||||
#'
|
||||
#' @param tracer The OpenTelemetry tracer to check (default: Shiny otel tracer)
|
||||
#' @return `TRUE` if tracing is enabled, `FALSE` otherwise
|
||||
#' @noRd
|
||||
otel_is_tracing_enabled <- function() {
|
||||
.globals[["otel_is_tracing_enabled"]]
|
||||
}
|
||||
|
||||
#' Shiny OpenTelemetry logger
|
||||
#'
|
||||
#' Used for logging OpenTelemetry events via `otel_log()`
|
||||
#' @return An OpenTelemetry logger
|
||||
#' @noRd
|
||||
shiny_otel_logger <- function() {
|
||||
.globals[["otel_logger"]]
|
||||
}
|
||||
|
||||
|
||||
|
||||
#' Shiny OpenTelemetry tracer
|
||||
#'
|
||||
#' Used for creating OpenTelemetry spans via `with_otel_span()` and
|
||||
#' `start_otel_span()`
|
||||
#'
|
||||
#' Inspired by httr2:::get_tracer().
|
||||
#' @return An OpenTelemetry tracer
|
||||
#' @noRd
|
||||
shiny_otel_tracer <- function() {
|
||||
.globals[["otel_tracer"]]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#' Create and use a Shiny OpenTelemetry span
|
||||
#'
|
||||
#' If otel is disabled, the span will not be created,
|
||||
#' however the expression will still be evaluated.
|
||||
#' @param name Span name
|
||||
#' @param expr Expression to evaluate within the span
|
||||
#' @param ... Ignored
|
||||
#' @param attributes Optional span attributes
|
||||
#' @return The result of evaluating `expr`
|
||||
#' @noRd
|
||||
with_otel_span <- function(name, expr, ..., attributes = NULL) {
|
||||
promises::with_otel_span(name, expr, ..., attributes = attributes, tracer = shiny_otel_tracer())
|
||||
}
|
||||
|
||||
|
||||
#' Start a Shiny OpenTelemetry span
|
||||
#'
|
||||
#' @param name Span name
|
||||
#' @param ... Additional arguments passed to `otel::start_span()`
|
||||
#' @return An OpenTelemetry span
|
||||
#' @noRd
|
||||
start_otel_span <- function(name, ...) {
|
||||
otel::start_span(name, ..., tracer = shiny_otel_tracer())
|
||||
}
|
||||
|
||||
|
||||
# # TODO: Set attributes on the current active span
|
||||
# # 5. Set attributes on the current active span
|
||||
# set_otel_span_attrs(status = 200L)
|
||||
|
||||
|
||||
# -- Helpers --------------------------------------------------------------
|
||||
|
||||
|
||||
is_otel_span <- function(x) {
|
||||
inherits(x, "otel_span")
|
||||
}
|
||||
|
||||
testthat__is_testing <- function() {
|
||||
# testthat::is_testing()
|
||||
identical(Sys.getenv("TESTTHAT"), "true")
|
||||
}
|
||||
|
||||
#' Log a message using the Shiny OpenTelemetry logger
|
||||
#'
|
||||
#' @param msg The log message
|
||||
#' @param ... Additional attributes to add to the log record
|
||||
#' @param severity The log severity level (default: "info")
|
||||
#' @param logger The OpenTelemetry logger to use (default: Shiny otel logger)
|
||||
#' @return Invisibly returns.
|
||||
#' @noRd
|
||||
otel_log <- function(
|
||||
msg,
|
||||
...,
|
||||
severity = "info",
|
||||
logger = shiny_otel_logger()
|
||||
) {
|
||||
otel::log(msg, ..., severity = severity, logger = logger)
|
||||
}
|
||||
125
R/otel-with.R
Normal file
125
R/otel-with.R
Normal file
@@ -0,0 +1,125 @@
|
||||
#' Temporarily set OpenTelemetry (OTel) collection level
|
||||
#'
|
||||
#' @description
|
||||
#' Control Shiny's OTel collection level for particular reactive expression(s).
|
||||
#'
|
||||
#' `withOtelCollect()` sets the OpenTelemetry collection level for
|
||||
#' the duration of evaluating `expr`. `localOtelCollect()` sets the collection
|
||||
#' level for the remainder of the current function scope.
|
||||
#'
|
||||
#' @details
|
||||
#' Note that `"session"` and `"reactive_update"` levels are not permitted as
|
||||
#' these are runtime-specific levels that should only be set permanently via
|
||||
#' `options(shiny.otel.collect = ...)` or the `SHINY_OTEL_COLLECT` environment
|
||||
#' variable, not temporarily during reactive expression creation.
|
||||
#'
|
||||
#' @section Best practice:
|
||||
#'
|
||||
#' Best practice is to set the collection level for code that *creates* reactive
|
||||
#' expressions, not code that *runs* them. For instance:
|
||||
#'
|
||||
#' ```r
|
||||
#' # Disable telemetry for a reactive expression
|
||||
#' withOtelCollect("none", {
|
||||
#' my_reactive <- reactive({ ... })
|
||||
#' })
|
||||
#'
|
||||
#' # Disable telemetry for a render function
|
||||
#' withOtelCollect("none", {
|
||||
#' output$my_plot <- renderPlot({ ... })
|
||||
#' })
|
||||
#'
|
||||
#' #' # Disable telemetry for an observer
|
||||
#' withOtelCollect("none", {
|
||||
#' observe({ ... }))
|
||||
#' })
|
||||
#'
|
||||
#' # Disable telemetry for an entire module
|
||||
#' withOtelCollect("none", {
|
||||
#' my_result <- my_module("my_id")
|
||||
#' })
|
||||
#' # Use `my_result` as normal here
|
||||
#' ```
|
||||
#'
|
||||
#' NOTE: It's not recommended to pipe existing reactive objects into
|
||||
#' `withOtelCollect()` since they won't inherit their intended OTel settings,
|
||||
#' leading to confusion.
|
||||
#'
|
||||
#' @param collect Character string specifying the OpenTelemetry collection level.
|
||||
#' Must be one of the following:
|
||||
#'
|
||||
#' * `"none"` - No telemetry data collected
|
||||
#' * `"reactivity"` - Collect reactive execution spans (includes session and
|
||||
#' reactive update events)
|
||||
#' * `"all"` - All available telemetry (currently equivalent to `"reactivity"`)
|
||||
#' @param expr Expression to evaluate with the specified collection level
|
||||
#' (for `withOtelCollect()`).
|
||||
#' @param envir Environment where the collection level should be set
|
||||
#' (for `localOtelCollect()`). Defaults to the parent frame.
|
||||
#'
|
||||
#' @return
|
||||
#' * `withOtelCollect()` returns the value of `expr`.
|
||||
#' * `localOtelCollect()` is called for its side effect and returns the previous
|
||||
#' `collect` value invisibly.
|
||||
#'
|
||||
#' @seealso See the `shiny.otel.collect` option within [`shinyOptions`]. Setting
|
||||
#' this value will globally control OpenTelemetry collection levels.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # Temporarily disable telemetry collection
|
||||
#' withOtelCollect("none", {
|
||||
#' # Code here won't generate telemetry
|
||||
#' reactive({ input$x + 1 })
|
||||
#' })
|
||||
#'
|
||||
#' # Collect reactivity telemetry but not other events
|
||||
#' withOtelCollect("reactivity", {
|
||||
#' # Reactive execution will be traced
|
||||
#' observe({ print(input$x) })
|
||||
#' })
|
||||
#'
|
||||
#' # Use local variant in a function
|
||||
#' my_function <- function() {
|
||||
#' localOtelCollect("none")
|
||||
#' # Rest of function executes without telemetry
|
||||
#' reactive({ input$y * 2 })
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @rdname withOtelCollect
|
||||
#' @export
|
||||
withOtelCollect <- function(collect, expr) {
|
||||
collect <- as_otel_collect_with(collect)
|
||||
|
||||
withr::with_options(
|
||||
list(shiny.otel.collect = collect),
|
||||
expr
|
||||
)
|
||||
}
|
||||
|
||||
#' @rdname withOtelCollect
|
||||
#' @export
|
||||
localOtelCollect <- function(collect, envir = parent.frame()) {
|
||||
collect <- as_otel_collect_with(collect)
|
||||
|
||||
old <- withr::local_options(
|
||||
list(shiny.otel.collect = collect),
|
||||
.local_envir = envir
|
||||
)
|
||||
|
||||
invisible(old)
|
||||
}
|
||||
|
||||
# Helper function to validate collect levels for with/local functions
|
||||
# Only allows "none", "reactivity", and "all" - not "session" or "reactive_update"
|
||||
as_otel_collect_with <- function(collect) {
|
||||
if (!is.character(collect)) {
|
||||
stop("`collect` must be a character vector.")
|
||||
}
|
||||
|
||||
allowed_levels <- c("none", "reactivity", "all")
|
||||
collect <- match.arg(collect, allowed_levels, several.ok = FALSE)
|
||||
|
||||
return(collect)
|
||||
}
|
||||
114
R/priorityqueue.R
Normal file
114
R/priorityqueue.R
Normal file
@@ -0,0 +1,114 @@
|
||||
# "...like a regular queue or stack data structure, but where additionally each
|
||||
# element has a "priority" associated with it. In a priority queue, an element
|
||||
# with high priority is served before an element with low priority. If two
|
||||
# elements have the same priority, they are served according to their order in
|
||||
# the queue." (http://en.wikipedia.org/wiki/Priority_queue)
|
||||
|
||||
PriorityQueue <- R6Class(
|
||||
'PriorityQueue',
|
||||
portable = FALSE,
|
||||
class = FALSE,
|
||||
public = list(
|
||||
# Keys are priorities, values are subqueues (implemented as list)
|
||||
.itemsByPriority = 'Map',
|
||||
# Sorted vector (largest first)
|
||||
.priorities = numeric(0),
|
||||
|
||||
initialize = function() {
|
||||
.itemsByPriority <<- Map$new()
|
||||
},
|
||||
# Enqueue an item, with the given priority level (must be integer). Higher
|
||||
# priority numbers are dequeued earlier than lower.
|
||||
enqueue = function(item, priority) {
|
||||
priority <- normalizePriority(priority)
|
||||
|
||||
if (!(priority %in% .priorities)) {
|
||||
.priorities <<- c(.priorities, priority)
|
||||
.priorities <<- sort(.priorities, decreasing=TRUE)
|
||||
.itemsByPriority$set(.key(priority), list(item))
|
||||
} else {
|
||||
.itemsByPriority$set(
|
||||
.key(priority),
|
||||
c(.itemsByPriority$get(.key(priority)), item)
|
||||
)
|
||||
}
|
||||
return(invisible())
|
||||
},
|
||||
# Retrieve a single item by 1) priority number (highest first) and then 2)
|
||||
# insertion order (first in, first out). If there are no items to be
|
||||
# dequeued, then NULL is returned. If it is necessary to distinguish between
|
||||
# a NULL value and the empty case, call isEmpty() before dequeue().
|
||||
dequeue = function() {
|
||||
if (length(.priorities) == 0)
|
||||
return(NULL)
|
||||
|
||||
maxPriority <- .priorities[[1]]
|
||||
items <- .itemsByPriority$get(.key(maxPriority))
|
||||
firstItem <- items[[1]]
|
||||
if (length(items) == 1) {
|
||||
# This is the last item at this priority. Remove both the list and the
|
||||
# priority level.
|
||||
.itemsByPriority$remove(.key(maxPriority))
|
||||
.priorities <<- .priorities[-1]
|
||||
} else {
|
||||
# There are still items at this priority. Remove the current item from
|
||||
# the list, and save it.
|
||||
items <- items[-1]
|
||||
.itemsByPriority$set(.key(maxPriority), items)
|
||||
}
|
||||
return(firstItem)
|
||||
},
|
||||
# Returns TRUE if no items are in the queue, otherwise FALSE.
|
||||
isEmpty = function() {
|
||||
length(.priorities) == 0
|
||||
},
|
||||
# Translates a priority integer to a character that is suitable for using as
|
||||
# a key.
|
||||
.key = function(priority) {
|
||||
sprintf('%a', priority)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
normalizePriority <- function(priority) {
|
||||
|
||||
if (is.null(priority))
|
||||
priority <- 0
|
||||
|
||||
# Cast integers to numeric to prevent any inconsistencies
|
||||
if (is.integer(priority))
|
||||
priority <- as.numeric(priority)
|
||||
|
||||
if (!is.numeric(priority))
|
||||
stop('priority must be an integer or numeric')
|
||||
|
||||
# Check length
|
||||
if (length(priority) == 0) {
|
||||
warning('Zero-length priority vector was passed; using 0')
|
||||
priority <- 0
|
||||
} else if (length(priority) > 1) {
|
||||
warning('Priority has length > 1 and only the first element will be used')
|
||||
priority <- priority[1]
|
||||
}
|
||||
|
||||
# NA == 0
|
||||
if (is.na(priority))
|
||||
priority <- 0
|
||||
|
||||
return(priority)
|
||||
}
|
||||
|
||||
# pq <- PriorityQueue$new()
|
||||
# pq$enqueue('a', 1)
|
||||
# pq$enqueue('b', 1L)
|
||||
# pq$enqueue('c', 1)
|
||||
# pq$enqueue('A', 2)
|
||||
# pq$enqueue('B', 2L)
|
||||
# pq$enqueue('C', 2)
|
||||
# pq$enqueue('d', 1)
|
||||
# pq$enqueue('e', 1L)
|
||||
# pq$enqueue('f', 1)
|
||||
# pq$enqueue('D', 2)
|
||||
# pq$enqueue('E', 2L)
|
||||
# pq$enqueue('F', 2)
|
||||
# # Expect ABCDEFabcdef
|
||||
322
R/progress.R
Normal file
322
R/progress.R
Normal file
@@ -0,0 +1,322 @@
|
||||
#' Reporting progress (object-oriented API)
|
||||
#'
|
||||
#' Reports progress to the user during long-running operations.
|
||||
#'
|
||||
#' This package exposes two distinct programming APIs for working with
|
||||
#' progress. [withProgress()] and [setProgress()]
|
||||
#' together provide a simple function-based interface, while the
|
||||
#' `Progress` reference class provides an object-oriented API.
|
||||
#'
|
||||
#' Instantiating a `Progress` object causes a progress panel to be
|
||||
#' created, and it will be displayed the first time the `set`
|
||||
#' method is called. Calling `close` will cause the progress panel
|
||||
#' to be removed.
|
||||
#'
|
||||
#' As of version 0.14, the progress indicators use Shiny's new notification API.
|
||||
#' If you want to use the old styling (for example, you may have used customized
|
||||
#' CSS), you can use `style="old"` each time you call
|
||||
#' `Progress$new()`. If you don't want to set the style each time
|
||||
#' `Progress$new` is called, you can instead call
|
||||
#' [`shinyOptions(progress.style="old")`][shinyOptions] just once, inside the server
|
||||
#' function.
|
||||
#'
|
||||
#' @param message A single-element character vector; the message to be
|
||||
#' displayed to the user, or `NULL` to hide the current message (if any).
|
||||
#' @param detail A single-element character vector; the detail message to be
|
||||
#' displayed to the user, or `NULL` to hide the current detail message (if
|
||||
#' any). The detail message will be shown with a de-emphasized appearance
|
||||
#' relative to `message`.
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' plotOutput("plot")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' output$plot <- renderPlot({
|
||||
#' progress <- Progress$new(session, min=1, max=15)
|
||||
#' on.exit(progress$close())
|
||||
#'
|
||||
#' progress$set(message = 'Calculation in progress',
|
||||
#' detail = 'This may take a while...')
|
||||
#'
|
||||
#' for (i in 1:15) {
|
||||
#' progress$set(value = i)
|
||||
#' Sys.sleep(0.5)
|
||||
#' }
|
||||
#' plot(cars)
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @seealso [withProgress()]
|
||||
#' @format NULL
|
||||
#' @usage NULL
|
||||
#' @export
|
||||
Progress <- R6Class(
|
||||
'Progress',
|
||||
public = list(
|
||||
|
||||
#' @description Creates a new progress panel (but does not display it).
|
||||
#' @param session The Shiny session object, as provided by `shinyServer` to
|
||||
#' the server function.
|
||||
#' @param min The value that represents the starting point of the progress
|
||||
#' bar. Must be less than `max`.
|
||||
#' @param max The value that represents the end of the progress bar. Must be
|
||||
#' greater than `min`.
|
||||
#' @param style Progress display style. If `"notification"` (the default),
|
||||
#' the progress indicator will show using Shiny's notification API. If
|
||||
#' `"old"`, use the same HTML and CSS used in Shiny 0.13.2 and below (this
|
||||
#' is for backward-compatibility).
|
||||
initialize = function(session = getDefaultReactiveDomain(),
|
||||
min = 0, max = 1,
|
||||
style = getShinyOption("progress.style", default = "notification"))
|
||||
{
|
||||
if (is.null(session))
|
||||
rlang::abort("Can only use Progress$new() inside a Shiny app")
|
||||
if (is.null(session$progressStack))
|
||||
rlang::abort("`session` is not a ShinySession object.")
|
||||
|
||||
private$session <- session
|
||||
private$id <- createUniqueId(8)
|
||||
private$min <- min
|
||||
private$max <- max
|
||||
private$value <- NULL
|
||||
private$style <- match.arg(style, choices = c("notification", "old"))
|
||||
private$closed <- FALSE
|
||||
|
||||
session$sendProgress('open', list(id = private$id, style = private$style))
|
||||
},
|
||||
|
||||
#' @description Updates the progress panel. When called the first time, the
|
||||
#' progress panel is displayed.
|
||||
#' @param value Single-element numeric vector; the value at which to set the
|
||||
#' progress bar, relative to `min` and `max`. `NULL` hides the progress
|
||||
#' bar, if it is currently visible.
|
||||
set = function(value = NULL, message = NULL, detail = NULL) {
|
||||
if (private$closed) {
|
||||
warning("Attempting to set progress, but progress already closed.")
|
||||
return()
|
||||
}
|
||||
|
||||
if (is.null(value) || is.na(value))
|
||||
value <- NULL
|
||||
|
||||
if (!is.null(value)) {
|
||||
private$value <- value
|
||||
# Normalize value to number between 0 and 1
|
||||
value <- min(1, max(0, (value - private$min) / (private$max - private$min)))
|
||||
}
|
||||
|
||||
data <- dropNulls(list(
|
||||
id = private$id,
|
||||
message = message,
|
||||
detail = detail,
|
||||
value = value,
|
||||
style = private$style
|
||||
))
|
||||
|
||||
private$session$sendProgress('update', data)
|
||||
},
|
||||
|
||||
#' @description Like `set`, this updates the progress panel. The difference
|
||||
#' is that `inc` increases the progress bar by `amount`, instead of
|
||||
#' setting it to a specific value.
|
||||
#' @param amount For the `inc()` method, a numeric value to increment the
|
||||
#' progress bar.
|
||||
inc = function(amount = 0.1, message = NULL, detail = NULL) {
|
||||
if (is.null(private$value))
|
||||
private$value <- private$min
|
||||
|
||||
value <- min(private$value + amount, private$max)
|
||||
self$set(value, message, detail)
|
||||
},
|
||||
|
||||
#' @description Returns the minimum value.
|
||||
getMin = function() private$min,
|
||||
|
||||
#' @description Returns the maximum value.
|
||||
getMax = function() private$max,
|
||||
|
||||
#' @description Returns the current value.
|
||||
getValue = function() private$value,
|
||||
|
||||
#' @description Removes the progress panel. Future calls to `set` and
|
||||
#' `close` will be ignored.
|
||||
close = function() {
|
||||
if (private$closed) {
|
||||
warning("Attempting to close progress, but progress already closed.")
|
||||
return()
|
||||
}
|
||||
|
||||
private$session$sendProgress('close',
|
||||
list(id = private$id, style = private$style)
|
||||
)
|
||||
private$closed <- TRUE
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
session = 'ShinySession',
|
||||
id = character(0),
|
||||
min = numeric(0),
|
||||
max = numeric(0),
|
||||
style = character(0),
|
||||
value = numeric(0),
|
||||
closed = logical(0)
|
||||
)
|
||||
)
|
||||
|
||||
#' Reporting progress (functional API)
|
||||
#'
|
||||
#' Reports progress to the user during long-running operations.
|
||||
#'
|
||||
#' This package exposes two distinct programming APIs for working with progress.
|
||||
#' Using `withProgress` with `incProgress` or `setProgress`
|
||||
#' provide a simple function-based interface, while the [Progress()]
|
||||
#' reference class provides an object-oriented API.
|
||||
#'
|
||||
#' Use `withProgress` to wrap the scope of your work; doing so will cause a
|
||||
#' new progress panel to be created, and it will be displayed the first time
|
||||
#' `incProgress` or `setProgress` are called. When `withProgress`
|
||||
#' exits, the corresponding progress panel will be removed.
|
||||
#'
|
||||
#' The `incProgress` function increments the status bar by a specified
|
||||
#' amount, whereas the `setProgress` function sets it to a specific value,
|
||||
#' and can also set the text displayed.
|
||||
#'
|
||||
#' Generally, `withProgress`/`incProgress`/`setProgress` should
|
||||
#' be sufficient; the exception is if the work to be done is asynchronous (this
|
||||
#' is not common) or otherwise cannot be encapsulated by a single scope. In that
|
||||
#' case, you can use the `Progress` reference class.
|
||||
#'
|
||||
#' As of version 0.14, the progress indicators use Shiny's new notification API.
|
||||
#' If you want to use the old styling (for example, you may have used customized
|
||||
#' CSS), you can use `style="old"` each time you call
|
||||
#' `withProgress()`. If you don't want to set the style each time
|
||||
#' `withProgress` is called, you can instead call
|
||||
#' [`shinyOptions(progress.style="old")`][shinyOptions] just once, inside the server
|
||||
#' function.
|
||||
#'
|
||||
#' @param session The Shiny session object, as provided by `shinyServer` to
|
||||
#' the server function. The default is to automatically find the session by
|
||||
#' using the current reactive domain.
|
||||
#' @param expr The work to be done. This expression should contain calls to
|
||||
#' [setProgress()] or [incProgress()].
|
||||
#' @param min The value that represents the starting point of the progress bar.
|
||||
#' Must be less tham `max`. Default is 0.
|
||||
#' @param max The value that represents the end of the progress bar. Must be
|
||||
#' greater than `min`. Default is 1.
|
||||
#' @param amount For `incProgress`, the amount to increment the status bar.
|
||||
#' Default is 0.1.
|
||||
#' @param env The environment in which `expr` should be evaluated.
|
||||
#' @param quoted Whether `expr` is a quoted expression (this is not
|
||||
#' common).
|
||||
#' @param message A single-element character vector; the message to be displayed
|
||||
#' to the user, or `NULL` to hide the current message (if any).
|
||||
#' @param detail A single-element character vector; the detail message to be
|
||||
#' displayed to the user, or `NULL` to hide the current detail message
|
||||
#' (if any). The detail message will be shown with a de-emphasized appearance
|
||||
#' relative to `message`.
|
||||
#' @param style Progress display style. If `"notification"` (the default),
|
||||
#' the progress indicator will show using Shiny's notification API. If
|
||||
#' `"old"`, use the same HTML and CSS used in Shiny 0.13.2 and below
|
||||
#' (this is for backward-compatibility).
|
||||
#' @param value Single-element numeric vector; the value at which to set the
|
||||
#' progress bar, relative to `min` and `max`.
|
||||
#'
|
||||
#' @return The result of `expr`.
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' plotOutput("plot")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$plot <- renderPlot({
|
||||
#' withProgress(message = 'Calculation in progress',
|
||||
#' detail = 'This may take a while...', value = 0, {
|
||||
#' for (i in 1:15) {
|
||||
#' incProgress(1/15)
|
||||
#' Sys.sleep(0.25)
|
||||
#' }
|
||||
#' })
|
||||
#' plot(cars)
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @seealso [Progress()]
|
||||
#' @rdname withProgress
|
||||
#' @export
|
||||
withProgress <- function(expr, min = 0, max = 1,
|
||||
value = min + (max - min) * 0.1,
|
||||
message = NULL, detail = NULL,
|
||||
style = getShinyOption("progress.style", default = "notification"),
|
||||
session = getDefaultReactiveDomain(),
|
||||
env = parent.frame(), quoted = FALSE)
|
||||
{
|
||||
|
||||
if (!quoted)
|
||||
expr <- substitute(expr)
|
||||
|
||||
if (is.null(session$progressStack))
|
||||
stop("'session' is not a ShinySession object.")
|
||||
|
||||
style <- match.arg(style, c("notification", "old"))
|
||||
|
||||
p <- Progress$new(session, min = min, max = max, style = style)
|
||||
|
||||
session$progressStack$push(p)
|
||||
on.exit({
|
||||
session$progressStack$pop()
|
||||
p$close()
|
||||
})
|
||||
|
||||
p$set(value, message, detail)
|
||||
|
||||
eval(expr, env)
|
||||
}
|
||||
|
||||
#' @rdname withProgress
|
||||
#' @export
|
||||
setProgress <- function(value = NULL, message = NULL, detail = NULL,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
if (is.null(session$progressStack))
|
||||
stop("'session' is not a ShinySession object.")
|
||||
|
||||
if (session$progressStack$size() == 0) {
|
||||
warning('setProgress was called outside of withProgress; ignoring')
|
||||
return()
|
||||
}
|
||||
|
||||
session$progressStack$peek()$set(value, message, detail)
|
||||
invisible()
|
||||
}
|
||||
|
||||
#' @rdname withProgress
|
||||
#' @export
|
||||
incProgress <- function(amount = 0.1, message = NULL, detail = NULL,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
if (is.null(session$progressStack))
|
||||
stop("'session' is not a ShinySession object.")
|
||||
|
||||
if (session$progressStack$size() == 0) {
|
||||
warning('incProgress was called outside of withProgress; ignoring')
|
||||
return()
|
||||
}
|
||||
|
||||
p <- session$progressStack$peek()
|
||||
p$inc(amount, message, detail)
|
||||
invisible()
|
||||
}
|
||||
329
R/react.R
329
R/react.R
@@ -1,123 +1,271 @@
|
||||
Context <- setRefClass(
|
||||
processId <- local({
|
||||
# pid is not sufficient to uniquely identify a process, because
|
||||
# distributed futures span machines which could introduce pid
|
||||
# collisions.
|
||||
cached <- NULL
|
||||
function() {
|
||||
if (is.null(cached)) {
|
||||
cached <<- rlang::hash(list(
|
||||
Sys.info(),
|
||||
Sys.time()
|
||||
))
|
||||
}
|
||||
# Sys.getpid() cannot be cached because forked children will
|
||||
# then have the same processId as their parents.
|
||||
paste(cached, Sys.getpid())
|
||||
}
|
||||
})
|
||||
|
||||
ctx_otel_info_obj <- function(
|
||||
isRecordingOtel = FALSE,
|
||||
otelLabel = "<unknown>",
|
||||
otelAttrs = list()
|
||||
) {
|
||||
structure(
|
||||
list(
|
||||
isRecordingOtel = isRecordingOtel,
|
||||
otelLabel = otelLabel,
|
||||
otelAttrs = otelAttrs
|
||||
),
|
||||
class = "ctx_otel_info"
|
||||
)
|
||||
}
|
||||
|
||||
with_otel_span_context <- function(otel_info, expr, domain) {
|
||||
if (!otel_is_tracing_enabled()) {
|
||||
return(force(expr))
|
||||
}
|
||||
|
||||
isRecordingOtel <- .subset2(otel_info, "isRecordingOtel")
|
||||
otelLabel <- .subset2(otel_info, "otelLabel")
|
||||
otelAttrs <- .subset2(otel_info, "otelAttrs")
|
||||
|
||||
# Always set the reactive update span as active
|
||||
# This ensures that any spans created within the reactive context
|
||||
# are at least children of the reactive update span
|
||||
maybe_with_otel_span_reactive_update(domain = domain, {
|
||||
if (isRecordingOtel) {
|
||||
with_otel_span(
|
||||
otelLabel,
|
||||
{
|
||||
# Works with both sync and async expressions
|
||||
# Needed for both observer and reactive contexts
|
||||
hybrid_then(
|
||||
expr,
|
||||
on_failure = set_otel_exception_status_and_throw,
|
||||
# Must upgrade the error object
|
||||
tee = FALSE
|
||||
)
|
||||
},
|
||||
# expr,
|
||||
attributes = otelAttrs
|
||||
)
|
||||
} else {
|
||||
force(expr)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#' @include graph.R
|
||||
Context <- R6Class(
|
||||
'Context',
|
||||
fields = list(
|
||||
id = 'character',
|
||||
.invalidated = 'logical',
|
||||
.callbacks = 'list',
|
||||
.hintCallbacks = 'list'
|
||||
),
|
||||
methods = list(
|
||||
initialize = function() {
|
||||
id <<- .getReactiveEnvironment()$nextId()
|
||||
.invalidated <<- F
|
||||
.callbacks <<- list()
|
||||
.hintCallbacks <<- list()
|
||||
portable = FALSE,
|
||||
class = FALSE,
|
||||
public = list(
|
||||
id = character(0),
|
||||
.reactId = character(0),
|
||||
.reactType = "other",
|
||||
.label = character(0), # For debug purposes
|
||||
.invalidated = FALSE,
|
||||
.invalidateCallbacks = list(),
|
||||
.flushCallbacks = list(),
|
||||
.domain = NULL,
|
||||
.pid = NULL,
|
||||
.weak = NULL,
|
||||
|
||||
.otel_info = NULL,
|
||||
|
||||
initialize = function(
|
||||
domain, label='', type='other', prevId='',
|
||||
reactId = rLog$noReactId,
|
||||
id = .getReactiveEnvironment()$nextId(), # For dummy context
|
||||
weak = FALSE,
|
||||
otel_info = ctx_otel_info_obj()
|
||||
) {
|
||||
id <<- id
|
||||
.label <<- label
|
||||
.domain <<- domain
|
||||
.pid <<- processId()
|
||||
.reactId <<- reactId
|
||||
.reactType <<- type
|
||||
.weak <<- weak
|
||||
rLog$createContext(id, label, type, prevId, domain)
|
||||
if (!is.null(otel_info)) {
|
||||
if (IS_SHINY_LOCAL_PKG) {
|
||||
stopifnot(inherits(otel_info, "ctx_otel_info"))
|
||||
}
|
||||
.otel_info <<- otel_info
|
||||
}
|
||||
},
|
||||
run = function(func) {
|
||||
"Run the provided function under this context."
|
||||
env <- .getReactiveEnvironment()
|
||||
env$runWith(.self, func)
|
||||
},
|
||||
invalidateHint = function() {
|
||||
"Let this context know it may or may not be invalidated very soon; that
|
||||
is, something in its dependency graph has been invalidated but there's no
|
||||
guarantee that the cascade of invalidations will reach all the way here.
|
||||
This is used to show progress in the UI."
|
||||
lapply(.hintCallbacks, function(func) {
|
||||
func()
|
||||
|
||||
# Use `promises::` as it shows up in the stack trace
|
||||
promises::with_promise_domain(reactivePromiseDomain(), {
|
||||
withReactiveDomain(.domain, {
|
||||
with_otel_span_context(.otel_info, domain = .domain, {
|
||||
captureStackTraces({
|
||||
env <- .getReactiveEnvironment()
|
||||
rLog$enter(.reactId, id, .reactType, .domain)
|
||||
on.exit(rLog$exit(.reactId, id, .reactType, .domain), add = TRUE)
|
||||
env$runWith(self, func)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
invalidate = function() {
|
||||
"Schedule this context for invalidation. It will not actually be
|
||||
invalidated until the next call to \\code{\\link{flushReact}}."
|
||||
"Invalidate this context. It will immediately call the callbacks
|
||||
that have been registered with onInvalidate()."
|
||||
|
||||
if (!identical(.pid, processId())) {
|
||||
rlang::abort("Reactive context was created in one process and invalidated from another.")
|
||||
}
|
||||
|
||||
if (.invalidated)
|
||||
return()
|
||||
.invalidated <<- T
|
||||
.getReactiveEnvironment()$addPendingInvalidate(.self)
|
||||
.invalidated <<- TRUE
|
||||
|
||||
rLog$invalidateStart(.reactId, id, .reactType, .domain)
|
||||
on.exit(rLog$invalidateEnd(.reactId, id, .reactType, .domain), add = TRUE)
|
||||
|
||||
lapply(.invalidateCallbacks, function(func) {
|
||||
func()
|
||||
})
|
||||
.invalidateCallbacks <<- list()
|
||||
NULL
|
||||
},
|
||||
onInvalidate = function(func) {
|
||||
"Register a function to be called when this context is invalidated.
|
||||
If this context is already invalidated, the function is called
|
||||
immediately."
|
||||
|
||||
if (!identical(.pid, processId())) {
|
||||
rlang::abort("Reactive context was created in one process and accessed from another.")
|
||||
}
|
||||
|
||||
if (.invalidated)
|
||||
func()
|
||||
else
|
||||
.callbacks <<- c(.callbacks, func)
|
||||
.invalidateCallbacks <<- c(.invalidateCallbacks, func)
|
||||
NULL
|
||||
},
|
||||
onInvalidateHint = function(func) {
|
||||
.hintCallbacks <<- c(.hintCallbacks, func)
|
||||
addPendingFlush = function(priority) {
|
||||
"Tell the reactive environment that this context should be flushed the
|
||||
next time flushReact() called."
|
||||
.getReactiveEnvironment()$addPendingFlush(self, priority)
|
||||
},
|
||||
executeCallbacks = function() {
|
||||
onFlush = function(func) {
|
||||
"Register a function to be called when this context is flushed."
|
||||
.flushCallbacks <<- c(.flushCallbacks, func)
|
||||
},
|
||||
executeFlushCallbacks = function() {
|
||||
"For internal use only."
|
||||
lapply(.callbacks, function(func) {
|
||||
tryCatch({
|
||||
func()
|
||||
}, warning = function(e) {
|
||||
# TODO: Callbacks in app
|
||||
print(e)
|
||||
}, error = function(e) {
|
||||
# TODO: Callbacks in app
|
||||
print(e)
|
||||
})
|
||||
|
||||
lapply(.flushCallbacks, function(flushCallback) {
|
||||
flushCallback()
|
||||
})
|
||||
},
|
||||
isWeak = function() {
|
||||
.weak
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
ReactiveEnvironment <- setRefClass(
|
||||
ReactiveEnvironment <- R6Class(
|
||||
'ReactiveEnvironment',
|
||||
fields = c('.currentContext', '.nextId', '.pendingInvalidate'),
|
||||
methods = list(
|
||||
portable = FALSE,
|
||||
class = FALSE,
|
||||
public = list(
|
||||
.currentContext = NULL,
|
||||
.nextId = 0L,
|
||||
.pendingFlush = 'PriorityQueue',
|
||||
.inFlush = FALSE,
|
||||
|
||||
initialize = function() {
|
||||
.currentContext <<- NULL
|
||||
.nextId <<- 0L
|
||||
.pendingInvalidate <<- list()
|
||||
.pendingFlush <<- PriorityQueue$new()
|
||||
},
|
||||
nextId = function() {
|
||||
.nextId <<- .nextId + 1L
|
||||
return(as.character(.nextId))
|
||||
},
|
||||
currentContext = function() {
|
||||
if (is.null(.currentContext))
|
||||
stop('Operation not allowed without an active reactive context. ',
|
||||
'(You tried to do something that can only be done from inside a ',
|
||||
'reactive function.)')
|
||||
if (is.null(.currentContext)) {
|
||||
if (isTRUE(getOption('shiny.suppressMissingContextError'))) {
|
||||
return(getDummyContext())
|
||||
} else {
|
||||
rlang::abort(c(
|
||||
'Operation not allowed without an active reactive context.',
|
||||
paste0(
|
||||
'You tried to do something that can only be done from inside a ',
|
||||
'reactive consumer.'
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
return(.currentContext)
|
||||
},
|
||||
runWith = function(ctx, func) {
|
||||
runWith = function(ctx, contextFunc) {
|
||||
old.ctx <- .currentContext
|
||||
.currentContext <<- ctx
|
||||
on.exit(.currentContext <<- old.ctx)
|
||||
func()
|
||||
contextFunc()
|
||||
},
|
||||
addPendingInvalidate = function(ctx) {
|
||||
.pendingInvalidate <<- c(.pendingInvalidate, ctx)
|
||||
addPendingFlush = function(ctx, priority) {
|
||||
.pendingFlush$enqueue(ctx, priority)
|
||||
},
|
||||
hasPendingFlush = function() {
|
||||
return(!.pendingFlush$isEmpty())
|
||||
},
|
||||
# Returns TRUE if anything was actually called
|
||||
flush = function() {
|
||||
while (length(.pendingInvalidate) > 0) {
|
||||
contexts <- .pendingInvalidate
|
||||
.pendingInvalidate <<- list()
|
||||
lapply(contexts, function(ctx) {
|
||||
ctx$executeCallbacks()
|
||||
NULL
|
||||
})
|
||||
# If nothing to flush, exit early
|
||||
if (!hasPendingFlush()) return(invisible(FALSE))
|
||||
# If already in a flush, don't start another one
|
||||
if (.inFlush) return(invisible(FALSE))
|
||||
.inFlush <<- TRUE
|
||||
on.exit({
|
||||
.inFlush <<- FALSE
|
||||
rLog$idle(domain = NULL)
|
||||
})
|
||||
|
||||
while (hasPendingFlush()) {
|
||||
ctx <- .pendingFlush$dequeue()
|
||||
ctx$executeFlushCallbacks()
|
||||
}
|
||||
|
||||
invisible(TRUE)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
.getReactiveEnvironment <- function() {
|
||||
if (!exists('.ReactiveEnvironment', envir=.GlobalEnv, inherits=F)) {
|
||||
assign('.ReactiveEnvironment', ReactiveEnvironment$new(), envir=.GlobalEnv)
|
||||
.getReactiveEnvironment <- local({
|
||||
reactiveEnvironment <- NULL
|
||||
function() {
|
||||
if (is.null(reactiveEnvironment))
|
||||
reactiveEnvironment <<- ReactiveEnvironment$new()
|
||||
return(reactiveEnvironment)
|
||||
}
|
||||
get('.ReactiveEnvironment', envir=.GlobalEnv, inherits=F)
|
||||
}
|
||||
})
|
||||
|
||||
# Causes any pending invalidations to run.
|
||||
# Causes any pending invalidations to run. Returns TRUE if any invalidations
|
||||
# were pending (i.e. if work was actually done).
|
||||
flushReact <- function() {
|
||||
.getReactiveEnvironment()$flush()
|
||||
return(.getReactiveEnvironment()$flush())
|
||||
}
|
||||
|
||||
# Retrieves the current reactive context, or errors if there is no reactive
|
||||
@@ -125,3 +273,46 @@ flushReact <- function() {
|
||||
getCurrentContext <- function() {
|
||||
.getReactiveEnvironment()$currentContext()
|
||||
}
|
||||
hasCurrentContext <- function() {
|
||||
!is.null(.getReactiveEnvironment()$.currentContext) ||
|
||||
isTRUE(getOption("shiny.suppressMissingContextError"))
|
||||
}
|
||||
|
||||
getDummyContext <- function() {
|
||||
Context$new(
|
||||
getDefaultReactiveDomain(), '[none]', type = 'isolate',
|
||||
id = "Dummy", reactId = rLog$dummyReactId
|
||||
)
|
||||
}
|
||||
|
||||
wrapForContext <- function(func, ctx) {
|
||||
force(func)
|
||||
force(ctx) # may be NULL (in the case of maskReactiveContext())
|
||||
|
||||
function(...) {
|
||||
.getReactiveEnvironment()$runWith(ctx, function() {
|
||||
func(...)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reactivePromiseDomain <- function() {
|
||||
new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
force(onFulfilled)
|
||||
|
||||
# ctx will be NULL if we're in a maskReactiveContext()
|
||||
ctx <- if (hasCurrentContext()) getCurrentContext() else NULL
|
||||
|
||||
wrapForContext(onFulfilled, ctx)
|
||||
},
|
||||
wrapOnRejected = function(onRejected) {
|
||||
force(onRejected)
|
||||
|
||||
# ctx will be NULL if we're in a maskReactiveContext()
|
||||
ctx <- if (hasCurrentContext()) getCurrentContext() else NULL
|
||||
|
||||
wrapForContext(onRejected, ctx)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
257
R/reactive-domains.R
Normal file
257
R/reactive-domains.R
Normal file
@@ -0,0 +1,257 @@
|
||||
#' @include globals.R
|
||||
NULL
|
||||
|
||||
#
|
||||
# Over the last few months we've seen a number of cases where it'd be helpful
|
||||
# for objects that are instantiated within a Shiny app to know what Shiny
|
||||
# session they are "owned" by. I put "owned" in quotes because there isn't a
|
||||
# built-in notion of object ownership in Shiny today, any more than there is a
|
||||
# notion of one object owning another in R.
|
||||
#
|
||||
# But it's intuitive to everyone, I think, that the outputs for a session are
|
||||
# owned by that session, and any logic that is executed as part of the output
|
||||
# is done on behalf of that session. And it seems like in the vast majority of
|
||||
# cases, observers that are created inside a shinyServer function (i.e. one per
|
||||
# session) are also intuitively owned by the session that's starting up.
|
||||
#
|
||||
# This notion of ownership is important/helpful for a few scenarios that have
|
||||
# come up in recent months:
|
||||
#
|
||||
# 1. The showcase mode that Jonathan implemented recently highlights
|
||||
# observers/reactives as they execute. In order for sessions to only receive
|
||||
# highlights for their own code execution, we need to know which sessions own
|
||||
# which observers. 2. We've seen a number of apps crash out when observers
|
||||
# outlive their sessions and then try to do things with their sessions (the
|
||||
# most common error message was something like "Can't write to a closed
|
||||
# websocket", but we now silently ignore writes to closed websockets). It'd be
|
||||
# convenient for the default behavior of observers to be that they don't
|
||||
# outlive their parent sessions. 3. The reactive log visualizer currently
|
||||
# visualizes all reactivity in the process; it would be great if by default it
|
||||
# only visualized the current session. 4. When an observer has an error, it
|
||||
# would be great to be able to send the error to the session so it can do its
|
||||
# own handling (such as sending the error info to the client so the user can be
|
||||
# notified). 5. Shiny Server Pro wants to show the admin how much time is being
|
||||
# spent servicing each session.
|
||||
#
|
||||
# So what are the rules for establishing ownership?
|
||||
#
|
||||
# 1. Define the "current domain" as a global variable whose value will own any
|
||||
# newly created observer (by default). A domain is a reference class or
|
||||
# environment that contains the functions `onEnded(callback)`, `isEnded()`, and
|
||||
# `reactlog(logEntry)`.
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
createMockDomain <- function() {
|
||||
callbacks <- Callbacks$new()
|
||||
ended <- FALSE
|
||||
domain <- new.env(parent = emptyenv())
|
||||
domain$ns <- function(id) id
|
||||
domain$token <- "mock-domain"
|
||||
domain$onEnded <- function(callback) {
|
||||
return(callbacks$register(callback))
|
||||
}
|
||||
domain$isEnded <- function() {
|
||||
ended
|
||||
}
|
||||
domain$reactlog <- function(logEntry) NULL
|
||||
domain$end <- function() {
|
||||
if (!ended) {
|
||||
ended <<- TRUE
|
||||
callbacks$invoke()
|
||||
}
|
||||
invisible()
|
||||
}
|
||||
domain$incrementBusyCount <- function() NULL
|
||||
domain$decrementBusyCount <- function() NULL
|
||||
return(domain)
|
||||
}
|
||||
|
||||
#
|
||||
# 2. The initial value of "current domain" is null.
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
.globals$domain <- NULL
|
||||
|
||||
#
|
||||
# 3. Objects that can be owned include observers, reactive expressions,
|
||||
# invalidateLater instances, reactiveTimer instances. Whenever one of these is
|
||||
# created, by default its owner will be the current domain.
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
|
||||
#' @name domains
|
||||
#' @rdname domains
|
||||
#' @export
|
||||
getDefaultReactiveDomain <- function() {
|
||||
.globals$domain
|
||||
}
|
||||
|
||||
#
|
||||
# 4. While a session is being created and the shinyServer function is executed,
|
||||
# the current domain is set to the new session. When the shinyServer function
|
||||
# is done executing, the previous value of the current domain is restored. This
|
||||
# is made foolproof using a `withReactiveDomain` function.
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
|
||||
#' @rdname domains
|
||||
#' @export
|
||||
withReactiveDomain <- function(domain, expr) {
|
||||
# Use `promises::` as it shows up in the stack trace
|
||||
promises::with_promise_domain(
|
||||
createVarPromiseDomain(.globals, "domain", domain),
|
||||
expr
|
||||
)
|
||||
}
|
||||
|
||||
#
|
||||
# 5. While an observer or reactive expression is executing, the current domain
|
||||
# is set to the owner of the observer. When the observer completes, the
|
||||
# previous value of the current domain is restored.
|
||||
#
|
||||
# 6. Note that once created, an observer/reactive expression belongs to the
|
||||
# same domain forever, regardless of how many times it is invalidated and
|
||||
# re-executed, and regardless of what caused the invalidation to happen.
|
||||
#
|
||||
# 7. When a session ends, any observers that it owns are suspended, any
|
||||
# invalidateLater/reactiveTimers are stopped.
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
|
||||
#' @rdname domains
|
||||
#' @export
|
||||
onReactiveDomainEnded <- function(domain, callback, failIfNull = FALSE) {
|
||||
if (is.null(domain)) {
|
||||
if (isTRUE(failIfNull))
|
||||
stop("onReactiveDomainEnded called with null domain and failIfNull=TRUE")
|
||||
else
|
||||
return()
|
||||
}
|
||||
domain$onEnded(callback)
|
||||
}
|
||||
|
||||
#
|
||||
# 8. If an uncaught error occurs while executing an observer, the session gets
|
||||
# a chance to handle it. I suppose the default behavior would be to send the
|
||||
# message to the client if possible, and then perhaps end the session (or not,
|
||||
# I could argue either way).
|
||||
#
|
||||
# The basic idea here is inspired by Node.js domains, which you can think of as
|
||||
# a way to track execution contexts across callback- or listener-oriented
|
||||
# asynchronous code. They use it to unify error handling code across a graph of
|
||||
# related objects. Our domains will be to unify both lifetime and error
|
||||
# handling across a graph of related reactive primitives.
|
||||
#
|
||||
# (You could imagine that as a client update is being processed, the session
|
||||
# associated with that client would become the current domain. IIRC this is how
|
||||
# showcase mode is implemented today. I don't think this would cover any cases
|
||||
# not covered by rule 5 above, and the absence of rule 5 would leave cases that
|
||||
# this rule would not cover.)
|
||||
#
|
||||
# Pitfalls/open issues:
|
||||
#
|
||||
# 1. Our current approach has the issue of observers staying alive longer than
|
||||
# they ought to. This proposal introduces the opposite risk: that
|
||||
# observers/invalidateLater/reactiveTimer instances, having implicitly been
|
||||
# assigned a parent, are suspended/disposed earlier than they ought to have
|
||||
# been. I find this especially worrisome for invalidateLater/reactiveTimer,
|
||||
# which will often be called in a reactive expression, and thus execute under
|
||||
# unpredictable circumstances. Perhaps those should continue to accept an
|
||||
# explicit "session=" parameter that the user is warned about if they don't
|
||||
# provide a value.
|
||||
#
|
||||
# 2. Are there situations where it is ambiguous what the right thing to do is,
|
||||
# and we should warn/error to ask the user to provide a domain explicitly?
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
|
||||
#' Reactive domains
|
||||
#'
|
||||
#' Reactive domains are a mechanism for establishing ownership over reactive
|
||||
#' primitives (like reactive expressions and observers), even if the set of
|
||||
#' reactive primitives is dynamically created. This is useful for lifetime
|
||||
#' management (i.e. destroying observers when the Shiny session that created
|
||||
#' them ends) and error handling.
|
||||
#'
|
||||
#' At any given time, there can be either a single "default" reactive domain
|
||||
#' object, or none (i.e. the reactive domain object is `NULL`). You can
|
||||
#' access the current default reactive domain by calling
|
||||
#' `getDefaultReactiveDomain`.
|
||||
#'
|
||||
#' Unless you specify otherwise, newly created observers and reactive
|
||||
#' expressions will be assigned to the current default domain (if any). You can
|
||||
#' override this assignment by providing an explicit `domain` argument to
|
||||
#' [reactive()] or [observe()].
|
||||
#'
|
||||
#' For advanced usage, it's possible to override the default domain using
|
||||
#' `withReactiveDomain`. The `domain` argument will be made the
|
||||
#' default domain while `expr` is evaluated.
|
||||
#'
|
||||
#' Implementers of new reactive primitives can use `onReactiveDomainEnded`
|
||||
#' as a convenience function for registering callbacks. If the reactive domain
|
||||
#' is `NULL` and `failIfNull` is `FALSE`, then the callback will
|
||||
#' never be invoked.
|
||||
#'
|
||||
#' @name domains
|
||||
#' @param domain A valid domain object (for example, a Shiny session), or
|
||||
#' `NULL`
|
||||
#' @param expr An expression to evaluate under `domain`
|
||||
#' @param callback A callback function to be invoked
|
||||
#' @param failIfNull If `TRUE` then an error is given if the `domain`
|
||||
#' is `NULL`
|
||||
NULL
|
||||
|
||||
#
|
||||
# Example 1
|
||||
# ---
|
||||
# ```
|
||||
# obs1 <- observe({
|
||||
# })
|
||||
# shinyServer(function(input, output) {
|
||||
# obs2 <- observe({
|
||||
# obs3 <- observe({
|
||||
# })
|
||||
# })
|
||||
# })
|
||||
# # obs1 would have no domain, obs2 and obs3 would be owned by the session
|
||||
# ```
|
||||
#
|
||||
# Example 2
|
||||
# ---
|
||||
# ```
|
||||
# globalValues <- reactiveValues(broadcast="")
|
||||
# shinyServer(function(input, output) {
|
||||
# sessionValues <- reactiveValues()
|
||||
# output$messageOutput <- renderText({
|
||||
# globalValues$broadcast
|
||||
# obs1 <- observe({...})
|
||||
# })
|
||||
# observe({
|
||||
# if (input$goButton == 0) return()
|
||||
# isolate( globalValues$broadcast <- input$messageInput )
|
||||
# })
|
||||
# })
|
||||
# # The observer behind messageOutput would be owned by the session,
|
||||
# # as would all the many instances of obs1 that were created.
|
||||
# ```
|
||||
# ---
|
||||
#
|
||||
# Example 3
|
||||
# ---
|
||||
# ```
|
||||
# rexpr1 <- reactive({
|
||||
# invalidateLater(1000)
|
||||
# obs1 <- observe({...})
|
||||
# })
|
||||
# observeSomething <- function() {
|
||||
# obs2 <- observe({...})
|
||||
# })
|
||||
# shinyServer(function(input, output) {
|
||||
# obs3 <- observe({
|
||||
# observeSomething()
|
||||
# rexpr1()
|
||||
# })
|
||||
# })
|
||||
# # rexpr1, the invalidateLater call, and obs1 would all have no owner;
|
||||
# # obs2 and obs3 would be owned by the session.
|
||||
# ```
|
||||
3029
R/reactives.R
3029
R/reactives.R
File diff suppressed because it is too large
Load Diff
201
R/reexports.R
Normal file
201
R/reexports.R
Normal file
@@ -0,0 +1,201 @@
|
||||
####
|
||||
# Generated by `./tools/documentation/updateReexports.R`: do not edit by hand
|
||||
# Please call `source('tools/documentation/updateReexports.R')` from the root folder to update`
|
||||
####
|
||||
|
||||
|
||||
# fastmap key_missing.Rd -------------------------------------------------------
|
||||
|
||||
#' @importFrom fastmap key_missing
|
||||
#' @export
|
||||
fastmap::key_missing
|
||||
|
||||
#' @importFrom fastmap is.key_missing
|
||||
#' @export
|
||||
fastmap::is.key_missing
|
||||
|
||||
|
||||
|
||||
# htmltools builder.Rd ---------------------------------------------------------
|
||||
|
||||
#' @importFrom htmltools tags
|
||||
#' @export
|
||||
htmltools::tags
|
||||
|
||||
#' @importFrom htmltools p
|
||||
#' @export
|
||||
htmltools::p
|
||||
|
||||
#' @importFrom htmltools h1
|
||||
#' @export
|
||||
htmltools::h1
|
||||
|
||||
#' @importFrom htmltools h2
|
||||
#' @export
|
||||
htmltools::h2
|
||||
|
||||
#' @importFrom htmltools h3
|
||||
#' @export
|
||||
htmltools::h3
|
||||
|
||||
#' @importFrom htmltools h4
|
||||
#' @export
|
||||
htmltools::h4
|
||||
|
||||
#' @importFrom htmltools h5
|
||||
#' @export
|
||||
htmltools::h5
|
||||
|
||||
#' @importFrom htmltools h6
|
||||
#' @export
|
||||
htmltools::h6
|
||||
|
||||
#' @importFrom htmltools a
|
||||
#' @export
|
||||
htmltools::a
|
||||
|
||||
#' @importFrom htmltools br
|
||||
#' @export
|
||||
htmltools::br
|
||||
|
||||
#' @importFrom htmltools div
|
||||
#' @export
|
||||
htmltools::div
|
||||
|
||||
#' @importFrom htmltools span
|
||||
#' @export
|
||||
htmltools::span
|
||||
|
||||
#' @importFrom htmltools pre
|
||||
#' @export
|
||||
htmltools::pre
|
||||
|
||||
#' @importFrom htmltools code
|
||||
#' @export
|
||||
htmltools::code
|
||||
|
||||
#' @importFrom htmltools img
|
||||
#' @export
|
||||
htmltools::img
|
||||
|
||||
#' @importFrom htmltools strong
|
||||
#' @export
|
||||
htmltools::strong
|
||||
|
||||
#' @importFrom htmltools em
|
||||
#' @export
|
||||
htmltools::em
|
||||
|
||||
#' @importFrom htmltools hr
|
||||
#' @export
|
||||
htmltools::hr
|
||||
|
||||
#' @importFrom htmltools tag
|
||||
#' @export
|
||||
htmltools::tag
|
||||
|
||||
|
||||
# htmltools tagList.Rd ---------------------------------------------------------
|
||||
|
||||
#' @importFrom htmltools tagList
|
||||
#' @export
|
||||
htmltools::tagList
|
||||
|
||||
|
||||
# htmltools tagAppendAttributes.Rd ---------------------------------------------
|
||||
|
||||
#' @importFrom htmltools tagAppendAttributes
|
||||
#' @export
|
||||
htmltools::tagAppendAttributes
|
||||
|
||||
#' @importFrom htmltools tagHasAttribute
|
||||
#' @export
|
||||
htmltools::tagHasAttribute
|
||||
|
||||
#' @importFrom htmltools tagGetAttribute
|
||||
#' @export
|
||||
htmltools::tagGetAttribute
|
||||
|
||||
|
||||
# htmltools tagAppendChild.Rd --------------------------------------------------
|
||||
|
||||
#' @importFrom htmltools tagAppendChild
|
||||
#' @export
|
||||
htmltools::tagAppendChild
|
||||
|
||||
#' @importFrom htmltools tagAppendChildren
|
||||
#' @export
|
||||
htmltools::tagAppendChildren
|
||||
|
||||
#' @importFrom htmltools tagSetChildren
|
||||
#' @export
|
||||
htmltools::tagSetChildren
|
||||
|
||||
|
||||
# htmltools HTML.Rd ------------------------------------------------------------
|
||||
|
||||
#' @importFrom htmltools HTML
|
||||
#' @export
|
||||
htmltools::HTML
|
||||
|
||||
|
||||
# htmltools include.Rd ---------------------------------------------------------
|
||||
|
||||
#' @importFrom htmltools includeHTML
|
||||
#' @export
|
||||
htmltools::includeHTML
|
||||
|
||||
#' @importFrom htmltools includeText
|
||||
#' @export
|
||||
htmltools::includeText
|
||||
|
||||
#' @importFrom htmltools includeMarkdown
|
||||
#' @export
|
||||
htmltools::includeMarkdown
|
||||
|
||||
#' @importFrom htmltools includeCSS
|
||||
#' @export
|
||||
htmltools::includeCSS
|
||||
|
||||
#' @importFrom htmltools includeScript
|
||||
#' @export
|
||||
htmltools::includeScript
|
||||
|
||||
|
||||
# htmltools singleton.Rd -------------------------------------------------------
|
||||
|
||||
#' @importFrom htmltools singleton
|
||||
#' @export
|
||||
htmltools::singleton
|
||||
|
||||
#' @importFrom htmltools is.singleton
|
||||
#' @export
|
||||
htmltools::is.singleton
|
||||
|
||||
|
||||
# htmltools validateCssUnit.Rd -------------------------------------------------
|
||||
|
||||
#' @importFrom htmltools validateCssUnit
|
||||
#' @export
|
||||
htmltools::validateCssUnit
|
||||
|
||||
|
||||
# htmltools htmlTemplate.Rd ----------------------------------------------------
|
||||
|
||||
#' @importFrom htmltools htmlTemplate
|
||||
#' @export
|
||||
htmltools::htmlTemplate
|
||||
|
||||
|
||||
# htmltools suppressDependencies.Rd --------------------------------------------
|
||||
|
||||
#' @importFrom htmltools suppressDependencies
|
||||
#' @export
|
||||
htmltools::suppressDependencies
|
||||
|
||||
|
||||
# htmltools withTags.Rd --------------------------------------------------------
|
||||
|
||||
#' @importFrom htmltools withTags
|
||||
#' @export
|
||||
htmltools::withTags
|
||||
280
R/render-cached-plot.R
Normal file
280
R/render-cached-plot.R
Normal file
@@ -0,0 +1,280 @@
|
||||
#' Plot output with cached images
|
||||
#'
|
||||
#' Renders a reactive plot, with plot images cached to disk. As of Shiny 1.6.0,
|
||||
#' this is a shortcut for using [bindCache()] with [renderPlot()].
|
||||
#'
|
||||
#' `expr` is an expression that generates a plot, similar to that in
|
||||
#' `renderPlot`. Unlike with `renderPlot`, this expression does not
|
||||
#' take reactive dependencies. It is re-executed only when the cache key
|
||||
#' changes.
|
||||
#'
|
||||
#' `cacheKeyExpr` is an expression which, when evaluated, returns an object
|
||||
#' which will be serialized and hashed using the [rlang::hash()]
|
||||
#' function to generate a string that will be used as a cache key. This key is
|
||||
#' used to identify the contents of the plot: if the cache key is the same as a
|
||||
#' previous time, it assumes that the plot is the same and can be retrieved from
|
||||
#' the cache.
|
||||
#'
|
||||
#' This `cacheKeyExpr` is reactive, and so it will be re-evaluated when any
|
||||
#' upstream reactives are invalidated. This will also trigger re-execution of
|
||||
#' the plotting expression, `expr`.
|
||||
#'
|
||||
#' The key should consist of "normal" R objects, like vectors and lists. Lists
|
||||
#' should in turn contain other normal R objects. If the key contains
|
||||
#' environments, external pointers, or reference objects --- or even if it has
|
||||
#' such objects attached as attributes --- then it is possible that it will
|
||||
#' change unpredictably even when you do not expect it to. Additionally, because
|
||||
#' the entire key is serialized and hashed, if it contains a very large object
|
||||
#' --- a large data set, for example --- there may be a noticeable performance
|
||||
#' penalty.
|
||||
#'
|
||||
#' If you face these issues with the cache key, you can work around them by
|
||||
#' extracting out the important parts of the objects, and/or by converting them
|
||||
#' to normal R objects before returning them. Your expression could even
|
||||
#' serialize and hash that information in an efficient way and return a string,
|
||||
#' which will in turn be hashed (very quickly) by the
|
||||
#' [rlang::hash()] function.
|
||||
#'
|
||||
#' Internally, the result from `cacheKeyExpr` is combined with the name of
|
||||
#' the output (if you assign it to `output$plot1`, it will be combined
|
||||
#' with `"plot1"`) to form the actual key that is used. As a result, even
|
||||
#' if there are multiple plots that have the same `cacheKeyExpr`, they
|
||||
#' will not have cache key collisions.
|
||||
#'
|
||||
#' @section Interactive plots:
|
||||
#'
|
||||
#' `renderCachedPlot` can be used to create interactive plots. See
|
||||
#' [plotOutput()] for more information and examples.
|
||||
#'
|
||||
#'
|
||||
#' @inheritParams renderPlot
|
||||
#' @inheritParams bindCache
|
||||
#' @param cacheKeyExpr An expression that returns a cache key. This key should
|
||||
#' be a unique identifier for a plot: the assumption is that if the cache key
|
||||
#' is the same, then the plot will be the same.
|
||||
#' @param sizePolicy A function that takes two arguments, `width` and
|
||||
#' `height`, and returns a list with `width` and `height`. The
|
||||
#' purpose is to round the actual pixel dimensions from the browser to some
|
||||
#' other dimensions, so that this will not generate and cache images of every
|
||||
#' possible pixel dimension. See [sizeGrowthRatio()] for more
|
||||
#' information on the default sizing policy.
|
||||
#' @param res The resolution of the PNG, in pixels per inch.
|
||||
#' @param width,height not used. They are specified via the argument
|
||||
#' `sizePolicy`.
|
||||
#'
|
||||
#' @seealso See [renderPlot()] for the regular, non-cached version of this
|
||||
#' function. It can be used with [bindCache()] to get the same effect as
|
||||
#' `renderCachedPlot()`. For more about configuring caches, see
|
||||
#' [cachem::cache_mem()] and [cachem::cache_disk()].
|
||||
#'
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # A basic example that uses the default app-scoped memory cache.
|
||||
#' # The cache will be shared among all simultaneous users of the application.
|
||||
#' shinyApp(
|
||||
#' fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
|
||||
#' ),
|
||||
#' mainPanel(plotOutput("plot"))
|
||||
#' )
|
||||
#' ),
|
||||
#' function(input, output, session) {
|
||||
#' output$plot <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) }
|
||||
#' )
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#'
|
||||
#' # An example uses a data object shared across sessions. mydata() is part of
|
||||
#' # the cache key, so when its value changes, plots that were previously
|
||||
#' # stored in the cache will no longer be used (unless mydata() changes back
|
||||
#' # to its previous value).
|
||||
#' mydata <- reactiveVal(data.frame(x = rnorm(400), y = rnorm(400)))
|
||||
#'
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("n", "Number of points", 50, 400, 100, step = 50),
|
||||
#' actionButton("newdata", "New data")
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' plotOutput("plot")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$newdata, {
|
||||
#' mydata(data.frame(x = rnorm(400), y = rnorm(400)))
|
||||
#' })
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' {
|
||||
#' Sys.sleep(2)
|
||||
#' d <- mydata()
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(d$x[seqn], d$y[seqn], xlim = range(d$x), ylim = range(d$y))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n, mydata()) },
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#'
|
||||
#' # A basic application with two plots, where each plot in each session has
|
||||
#' # a separate cache.
|
||||
#' shinyApp(
|
||||
#' fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' plotOutput("plot1"),
|
||||
#' plotOutput("plot2")
|
||||
#' )
|
||||
#' )
|
||||
#' ),
|
||||
#' function(input, output, session) {
|
||||
#' output$plot1 <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) },
|
||||
#' cache = cachem::cache_mem()
|
||||
#' )
|
||||
#' output$plot2 <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
#' seqn <- seq_len(input$n)
|
||||
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) },
|
||||
#' cache = cachem::cache_mem()
|
||||
#' )
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' \dontrun{
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a memory
|
||||
#' # cache that is 20 MB in size, and where cached objects expire after one
|
||||
#' # hour.
|
||||
#' shinyOptions(cache = cachem::cache_mem(max_size = 20e6, max_age = 3600))
|
||||
#'
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a disk
|
||||
#' # cache that can be shared among multiple concurrent R processes, and is
|
||||
#' # deleted when the system reboots.
|
||||
#' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache")))
|
||||
#'
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a disk
|
||||
#' # cache that can be shared among multiple concurrent R processes, and
|
||||
#' # persists on disk across reboots.
|
||||
#' shinyOptions(cache = cachem::cache_disk("./myapp-cache"))
|
||||
#'
|
||||
#' # At the top of the server function, this set the session-scoped cache to be
|
||||
#' # a memory cache that is 5 MB in size.
|
||||
#' server <- function(input, output, session) {
|
||||
#' shinyOptions(cache = cachem::cache_mem(max_size = 5e6))
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' ...,
|
||||
#' cache = "session"
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' }
|
||||
#' @export
|
||||
renderCachedPlot <- function(expr,
|
||||
cacheKeyExpr,
|
||||
sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2),
|
||||
res = 72,
|
||||
cache = "app",
|
||||
...,
|
||||
alt = "Plot object",
|
||||
outputArgs = list(),
|
||||
width = NULL,
|
||||
height = NULL
|
||||
) {
|
||||
|
||||
expr <- substitute(expr)
|
||||
if (!is_quosure(expr)) {
|
||||
expr <- new_quosure(expr, env = parent.frame())
|
||||
}
|
||||
|
||||
cacheKeyExpr <- substitute(cacheKeyExpr)
|
||||
if (!is_quosure(cacheKeyExpr)) {
|
||||
cacheKeyExpr <- new_quosure(cacheKeyExpr, env = parent.frame())
|
||||
}
|
||||
|
||||
if (!is.null(width) || !is.null(height)) {
|
||||
warning("Unused argument(s) 'width' and/or 'height'. ",
|
||||
"'sizePolicy' is used instead.")
|
||||
}
|
||||
|
||||
inject(
|
||||
bindCache(
|
||||
renderPlot(!!expr, res = res, alt = alt, outputArgs = outputArgs, ...),
|
||||
!!cacheKeyExpr,
|
||||
sizePolicy = sizePolicy,
|
||||
cache = cache
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' Create a sizing function that grows at a given ratio
|
||||
#'
|
||||
#' Returns a function which takes a two-element vector representing an input
|
||||
#' width and height, and returns a two-element vector of width and height. The
|
||||
#' possible widths are the base width times the growthRate to any integer power.
|
||||
#' For example, with a base width of 500 and growth rate of 1.25, the possible
|
||||
#' widths include 320, 400, 500, 625, 782, and so on, both smaller and larger.
|
||||
#' Sizes are rounded up to the next pixel. Heights are computed the same way as
|
||||
#' widths.
|
||||
#'
|
||||
#' @param width,height Base width and height.
|
||||
#' @param growthRate Growth rate multiplier.
|
||||
#'
|
||||
#' @seealso This is to be used with [renderCachedPlot()].
|
||||
#'
|
||||
#' @examples
|
||||
#' f <- sizeGrowthRatio(500, 500, 1.25)
|
||||
#' f(c(400, 400))
|
||||
#' f(c(500, 500))
|
||||
#' f(c(530, 550))
|
||||
#' f(c(625, 700))
|
||||
#'
|
||||
#' @export
|
||||
sizeGrowthRatio <- function(width = 400, height = 400, growthRate = 1.2) {
|
||||
round_dim_up <- function(x, base, rate) {
|
||||
power <- ceiling(log(x / base, rate))
|
||||
ceiling(base * rate^power)
|
||||
}
|
||||
|
||||
function(dims) {
|
||||
if (length(dims) != 2) {
|
||||
stop("dims must be a vector with two numbers, for width and height.")
|
||||
}
|
||||
c(
|
||||
round_dim_up(dims[1], width, growthRate),
|
||||
round_dim_up(dims[2], height, growthRate)
|
||||
)
|
||||
}
|
||||
}
|
||||
1148
R/render-plot.R
Normal file
1148
R/render-plot.R
Normal file
File diff suppressed because it is too large
Load Diff
246
R/render-table.R
Normal file
246
R/render-table.R
Normal file
@@ -0,0 +1,246 @@
|
||||
#' Table Output
|
||||
#'
|
||||
#' @description
|
||||
#' The `tableOuptut()`/`renderTable()` pair creates a reactive table that is
|
||||
#' suitable for display small matrices and data frames. The columns are
|
||||
#' formatted with [xtable::xtable()].
|
||||
#'
|
||||
#' See [renderDataTable()] for data frames that are too big to fit on a single
|
||||
#' page.
|
||||
#'
|
||||
#' @param expr An expression that returns an R object that can be used with
|
||||
#' [xtable::xtable()].
|
||||
#' @param striped,hover,bordered Logicals: if `TRUE`, apply the
|
||||
#' corresponding Bootstrap table format to the output table.
|
||||
#' @param spacing The spacing between the rows of the table (`xs`
|
||||
#' stands for "extra small", `s` for "small", `m` for "medium"
|
||||
#' and `l` for "large").
|
||||
#' @param width Table width. Must be a valid CSS unit (like "100%", "400px",
|
||||
#' "auto") or a number, which will be coerced to a string and
|
||||
#' have "px" appended.
|
||||
#' @param align A string that specifies the column alignment. If equal to
|
||||
#' `'l'`, `'c'` or `'r'`, then all columns will be,
|
||||
#' respectively, left-, center- or right-aligned. Otherwise, `align`
|
||||
#' must have the same number of characters as the resulting table (if
|
||||
#' `rownames = TRUE`, this will be equal to `ncol()+1`), with
|
||||
#' the *i*-th character specifying the alignment for the
|
||||
#' *i*-th column (besides `'l'`, `'c'` and
|
||||
#' `'r'`, `'?'` is also permitted - `'?'` is a placeholder
|
||||
#' for that particular column, indicating that it should keep its default
|
||||
#' alignment). If `NULL`, then all numeric/integer columns (including
|
||||
#' the row names, if they are numbers) will be right-aligned and
|
||||
#' everything else will be left-aligned (`align = '?'` produces the
|
||||
#' same result).
|
||||
#' @param rownames,colnames Logicals: include rownames? include colnames
|
||||
#' (column headers)?
|
||||
#' @param digits An integer specifying the number of decimal places for
|
||||
#' the numeric columns (this will not apply to columns with an integer
|
||||
#' class). If `digits` is set to a negative value, then the numeric
|
||||
#' columns will be displayed in scientific format with a precision of
|
||||
#' `abs(digits)` digits.
|
||||
#' @param na The string to use in the table cells whose values are missing
|
||||
#' (i.e. they either evaluate to `NA` or `NaN`).
|
||||
#' @param ... Arguments to be passed through to [xtable::xtable()]
|
||||
#' and [xtable::print.xtable()].
|
||||
#' @inheritParams renderUI
|
||||
#' @param outputArgs A list of arguments to be passed through to the
|
||||
#' implicit call to [tableOutput()] when `renderTable` is
|
||||
#' used in an interactive R Markdown document.
|
||||
#' @export
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' # table example
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' fluidRow(
|
||||
#' column(12,
|
||||
#' tableOutput('table')
|
||||
#' )
|
||||
#' )
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$table <- renderTable(iris)
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
renderTable <- function(expr, striped = FALSE, hover = FALSE,
|
||||
bordered = FALSE, spacing = c("s", "xs", "m", "l"),
|
||||
width = "auto", align = NULL,
|
||||
rownames = FALSE, colnames = TRUE,
|
||||
digits = NULL, na = "NA", ...,
|
||||
env = parent.frame(), quoted = FALSE,
|
||||
outputArgs=list())
|
||||
{
|
||||
func <- installExprFunction(expr, "func", env, quoted, label = "renderTable")
|
||||
|
||||
if (!is.function(spacing)) spacing <- match.arg(spacing)
|
||||
|
||||
# A small helper function to create a wrapper for an argument that was
|
||||
# passed to renderTable()
|
||||
createWrapper <- function(arg) {
|
||||
if (is.function(arg)) wrapper <- arg
|
||||
else wrapper <- function() arg
|
||||
return(wrapper)
|
||||
}
|
||||
|
||||
# Create wrappers for most arguments so that functions can also be passed
|
||||
# in, rather than only literals (useful for shiny apps)
|
||||
stripedWrapper <- createWrapper(striped)
|
||||
hoverWrapper <- createWrapper(hover)
|
||||
borderedWrapper <- createWrapper(bordered)
|
||||
spacingWrapper <- createWrapper(spacing)
|
||||
widthWrapper <- createWrapper(width)
|
||||
alignWrapper <- createWrapper(align)
|
||||
rownamesWrapper <- createWrapper(rownames)
|
||||
colnamesWrapper <- createWrapper(colnames)
|
||||
digitsWrapper <- createWrapper(digits)
|
||||
naWrapper <- createWrapper(na)
|
||||
|
||||
dots <- list(...) ## used later (but defined here because of scoping)
|
||||
|
||||
createRenderFunction(
|
||||
func,
|
||||
function(data, session, name, ...) {
|
||||
striped <- stripedWrapper()
|
||||
hover <- hoverWrapper()
|
||||
bordered <- borderedWrapper()
|
||||
format <- c(striped = striped, hover = hover, bordered = bordered)
|
||||
spacing <- spacingWrapper()
|
||||
width <- widthWrapper()
|
||||
align <- alignWrapper()
|
||||
rownames <- rownamesWrapper()
|
||||
colnames <- colnamesWrapper()
|
||||
digits <- digitsWrapper()
|
||||
na <- naWrapper()
|
||||
|
||||
spacing_choices <- c("s", "xs", "m", "l")
|
||||
if (!(spacing %in% spacing_choices)) {
|
||||
stop(paste("`spacing` must be one of",
|
||||
paste0("'", spacing_choices, "'", collapse=", ")))
|
||||
}
|
||||
|
||||
# For css styling
|
||||
classNames <- paste0("table shiny-table",
|
||||
paste0(" table-", names(format)[format], collapse = "" ),
|
||||
paste0(" spacing-", spacing))
|
||||
|
||||
data <- as.data.frame(data)
|
||||
|
||||
# Return NULL if no data is provided
|
||||
if (is.null(data) ||
|
||||
(is.data.frame(data) && nrow(data) == 0 && ncol(data) == 0))
|
||||
return(NULL)
|
||||
|
||||
# Separate the ... args to pass to xtable() vs print.xtable()
|
||||
xtable_argnames <- setdiff(names(formals(xtable)), c("x", "..."))
|
||||
xtable_args <- dots[intersect(names(dots), xtable_argnames)]
|
||||
non_xtable_args <- dots[setdiff(names(dots), xtable_argnames)]
|
||||
|
||||
# By default, numbers are right-aligned and everything else is left-aligned.
|
||||
defaultAlignment <- function(col) {
|
||||
if (is.numeric(col)) "r" else "l"
|
||||
}
|
||||
|
||||
# Figure out column alignment
|
||||
## Case 1: default alignment
|
||||
if (is.null(align) || align == "?") {
|
||||
names <- defaultAlignment(attr(data, "row.names"))
|
||||
cols <- paste(vapply(data, defaultAlignment, character(1)), collapse = "")
|
||||
cols <- paste0(names, cols)
|
||||
} else {
|
||||
## Case 2: user-specified alignment
|
||||
num_cols <- if (rownames) nchar(align) else nchar(align)+1
|
||||
valid <- !grepl("[^lcr\\?]", align)
|
||||
if (num_cols == ncol(data)+1 && valid) {
|
||||
cols <- if (rownames) align else paste0("r", align)
|
||||
defaults <- grep("\\?", strsplit(cols,"")[[1]])
|
||||
if (length(defaults) != 0) {
|
||||
vals <- vapply(data[,defaults-1], defaultAlignment, character(1))
|
||||
for (i in seq_len(length(defaults))) {
|
||||
substr(cols, defaults[i], defaults[i]) <- vals[i]
|
||||
}
|
||||
}
|
||||
} else if (nchar(align) == 1 && valid) {
|
||||
cols <- paste0(rep(align, ncol(data)+1), collapse="")
|
||||
} else {
|
||||
stop("`align` must contain only the characters `l`, `c`, `r` and/or `?` and",
|
||||
"have length either equal to 1 or to the total number of columns")
|
||||
}
|
||||
}
|
||||
|
||||
# Call xtable with its (updated) args
|
||||
xtable_args <- c(xtable_args, align = cols, digits = digits)
|
||||
xtable_res <- do.call(xtable, c(list(data), xtable_args))
|
||||
|
||||
# Set up print args
|
||||
print_args <- list(
|
||||
x = xtable_res,
|
||||
type = 'html',
|
||||
include.rownames = {
|
||||
if ("include.rownames" %in% names(dots)) dots$include.rownames
|
||||
else rownames
|
||||
},
|
||||
include.colnames = {
|
||||
if ("include.colnames" %in% names(dots)) dots$include.colnames
|
||||
else colnames
|
||||
},
|
||||
NA.string = {
|
||||
if ("NA.string" %in% names(dots)) dots$NA.string
|
||||
else na
|
||||
},
|
||||
html.table.attributes =
|
||||
paste0({
|
||||
if ("html.table.attributes" %in% names(dots)) dots$html.table.attributes
|
||||
else ""
|
||||
}, " ",
|
||||
"class = '", htmlEscape(classNames, TRUE), "' ",
|
||||
"style = 'width:", validateCssUnit(width), ";'"),
|
||||
comment = {
|
||||
if ("comment" %in% names(dots)) dots$comment
|
||||
else FALSE
|
||||
}
|
||||
)
|
||||
|
||||
print_args <- c(print_args, non_xtable_args)
|
||||
print_args <- print_args[unique(names(print_args))]
|
||||
|
||||
# Capture the raw html table returned by print.xtable(), and store it in
|
||||
# a variable for further processing
|
||||
tab <- paste(utils::capture.output(do.call(print, print_args)),collapse = "\n")
|
||||
|
||||
# Add extra class to cells with NA value, to be able to style them separately
|
||||
tab <- gsub(paste(">", na, "<"), paste(" class='NA'>", na, "<"), tab)
|
||||
|
||||
# All further processing concerns the table headers, so we don't need to run
|
||||
# any of this if colnames=FALSE
|
||||
if (colnames) {
|
||||
# Make sure that the final html table has a proper header (not included
|
||||
# in the print.xtable() default)
|
||||
tab <- sub("<tr>", "<thead> <tr>", tab)
|
||||
tab <- sub("</tr>", "</tr> </thead> <tbody>", tab)
|
||||
tab <- sub("</table>$", "</tbody> </table>", tab)
|
||||
|
||||
# Update the `cols` string (which stores the alignment of each column) so
|
||||
# that it only includes the alignment for the table variables (and not
|
||||
# for the row.names)
|
||||
cols <- if (rownames) cols else substr(cols, 2, nchar(cols))
|
||||
|
||||
# Create a vector whose i-th entry corresponds to the i-th table variable
|
||||
# alignment (substituting "l" by "left", "c" by "center" and "r" by "right")
|
||||
cols <- strsplit(cols, "")[[1]]
|
||||
cols[cols == "l"] <- "left"
|
||||
cols[cols == "r"] <- "right"
|
||||
cols[cols == "c"] <- "center"
|
||||
|
||||
# Align each header accordingly (this guarantees that each header and its
|
||||
# corresponding column have the same alignment)
|
||||
for (i in seq_len(length(cols))) {
|
||||
tab <- sub("<th>", paste0("<th style='text-align: ", cols[i], ";'>"), tab)
|
||||
}
|
||||
}
|
||||
return(tab)
|
||||
},
|
||||
tableOutput, outputArgs
|
||||
)
|
||||
}
|
||||
150
R/run-url.R
Normal file
150
R/run-url.R
Normal file
@@ -0,0 +1,150 @@
|
||||
#' Run a Shiny application from a URL
|
||||
#'
|
||||
#' `runUrl()` downloads and launches a Shiny application that is hosted at
|
||||
#' a downloadable URL. The Shiny application must be saved in a .zip, .tar, or
|
||||
#' .tar.gz file. The Shiny application files must be contained in the root
|
||||
#' directory or a subdirectory in the archive. For example, the files might be
|
||||
#' `myapp/server.r` and `myapp/ui.r`. The functions `runGitHub()`
|
||||
#' and `runGist()` are based on `runUrl()`, using URL's from GitHub
|
||||
#' (<https://github.com>) and GitHub gists (<https://gist.github.com>),
|
||||
#' respectively.
|
||||
#' @param url URL of the application.
|
||||
#' @param filetype The file type (`".zip"`, `".tar"`, or
|
||||
#' `".tar.gz"`. Defaults to the file extension taken from the url.
|
||||
#' @param subdir A subdirectory in the repository that contains the app. By
|
||||
#' default, this function will run an app from the top level of the repo, but
|
||||
#' you can use a path such as `"inst/shinyapp"`.
|
||||
#' @param destdir Directory to store the downloaded application files. If `NULL`
|
||||
#' (the default), the application files will be stored in a temporary directory
|
||||
#' and removed when the app exits
|
||||
#' @param ... Other arguments to be passed to [runApp()], such as
|
||||
#' `port` and `launch.browser`.
|
||||
#' @export
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' runUrl('https://github.com/rstudio/shiny_example/archive/main.tar.gz')
|
||||
#'
|
||||
#' # Can run an app from a subdirectory in the archive
|
||||
#' runUrl("https://github.com/rstudio/shiny_example/archive/main.zip",
|
||||
#' subdir = "inst/shinyapp/")
|
||||
#' }
|
||||
runUrl <- function(url, filetype = NULL, subdir = NULL, destdir = NULL, ...) {
|
||||
|
||||
if (!is.null(subdir) && ".." %in% strsplit(subdir, '/')[[1]])
|
||||
stop("'..' not allowed in subdir")
|
||||
|
||||
if (is.null(filetype))
|
||||
filetype <- basename(url)
|
||||
|
||||
if (grepl("\\.tar\\.gz$", filetype))
|
||||
fileext <- ".tar.gz"
|
||||
else if (grepl("\\.tar$", filetype))
|
||||
fileext <- ".tar"
|
||||
else if (grepl("\\.zip$", filetype))
|
||||
fileext <- ".zip"
|
||||
else
|
||||
stop("Unknown file extension.")
|
||||
|
||||
message("Downloading ", url)
|
||||
if (is.null(destdir)) {
|
||||
filePath <- tempfile('shinyapp', fileext = fileext)
|
||||
fileDir <- tempfile('shinyapp')
|
||||
} else {
|
||||
fileDir <- destdir
|
||||
filePath <- paste(destdir, fileext)
|
||||
}
|
||||
|
||||
dir.create(fileDir, showWarnings = FALSE)
|
||||
if (download(url, filePath, mode = "wb", quiet = TRUE) != 0)
|
||||
stop("Failed to download URL ", url)
|
||||
on.exit(unlink(filePath))
|
||||
|
||||
if (fileext %in% c(".tar", ".tar.gz")) {
|
||||
# Regular untar commonly causes two problems on Windows with github tarballs:
|
||||
# 1) If RTools' tar.exe is in the path, you get cygwin path warnings which
|
||||
# throw list=TRUE off;
|
||||
# 2) If the internal untar implementation is used, it chokes on the 'g'
|
||||
# type flag that github uses (to stash their commit hash info).
|
||||
# By using our own forked/modified untar2 we sidestep both issues.
|
||||
first <- untar2(filePath, list=TRUE)[1]
|
||||
untar2(filePath, exdir = fileDir)
|
||||
|
||||
} else if (fileext == ".zip") {
|
||||
first <- as.character(utils::unzip(filePath, list=TRUE)$Name)[1]
|
||||
utils::unzip(filePath, exdir = fileDir)
|
||||
}
|
||||
|
||||
if(is.null(destdir)){
|
||||
on.exit(unlink(fileDir, recursive = TRUE), add = TRUE)
|
||||
}
|
||||
|
||||
appdir <- file.path(fileDir, first)
|
||||
if (!utils::file_test('-d', appdir)) appdir <- dirname(appdir)
|
||||
|
||||
if (!is.null(subdir)) appdir <- file.path(appdir, subdir)
|
||||
runApp(appdir, ...)
|
||||
}
|
||||
|
||||
#' @rdname runUrl
|
||||
#' @param gist The identifier of the gist. For example, if the gist is
|
||||
#' https://gist.github.com/jcheng5/3239667, then `3239667`,
|
||||
#' `'3239667'`, and `'https://gist.github.com/jcheng5/3239667'` are
|
||||
#' all valid values.
|
||||
#' @export
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' runGist(3239667)
|
||||
#' runGist("https://gist.github.com/jcheng5/3239667")
|
||||
#'
|
||||
#' # Old URL format without username
|
||||
#' runGist("https://gist.github.com/3239667")
|
||||
#' }
|
||||
#'
|
||||
runGist <- function(gist, destdir = NULL, ...) {
|
||||
|
||||
gistUrl <- if (is.numeric(gist) || grepl('^[0-9a-f]+$', gist)) {
|
||||
sprintf('https://gist.github.com/%s/download', gist)
|
||||
} else if(grepl('^https://gist.github.com/([^/]+/)?([0-9a-f]+)$', gist)) {
|
||||
paste(gist, '/download', sep='')
|
||||
} else {
|
||||
stop('Unrecognized gist identifier format')
|
||||
}
|
||||
|
||||
runUrl(gistUrl, filetype = ".zip", destdir = destdir, ...)
|
||||
}
|
||||
|
||||
|
||||
#' @rdname runUrl
|
||||
#' @param repo Name of the repository.
|
||||
#' @param username GitHub username. If `repo` is of the form
|
||||
#' `"username/repo"`, `username` will be taken from `repo`.
|
||||
#' @param ref Desired git reference. Could be a commit, tag, or branch name.
|
||||
#' Defaults to `"HEAD"`, which means the default branch on GitHub, typically
|
||||
#' `"main"` or `"master"`.
|
||||
#' @export
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' runGitHub("shiny_example", "rstudio")
|
||||
#' # or runGitHub("rstudio/shiny_example")
|
||||
#'
|
||||
#' # Can run an app from a subdirectory in the repo
|
||||
#' runGitHub("shiny_example", "rstudio", subdir = "inst/shinyapp/")
|
||||
#' }
|
||||
runGitHub <- function(repo, username = getOption("github.user"),
|
||||
ref = "HEAD", subdir = NULL, destdir = NULL, ...) {
|
||||
|
||||
if (grepl('/', repo)) {
|
||||
res <- strsplit(repo, '/')[[1]]
|
||||
if (length(res) != 2) stop("'repo' must be of the form 'username/repo'")
|
||||
username <- res[1]
|
||||
repo <- res[2]
|
||||
}
|
||||
|
||||
url <- paste("https://github.com/", username, "/", repo, "/archive/",
|
||||
ref, ".tar.gz", sep = "")
|
||||
|
||||
runUrl(url, subdir = subdir, destdir = destdir, ...)
|
||||
}
|
||||
601
R/runapp.R
Normal file
601
R/runapp.R
Normal file
@@ -0,0 +1,601 @@
|
||||
#' Run Shiny Application
|
||||
#'
|
||||
#' Runs a Shiny application. This function normally does not return; interrupt R
|
||||
#' to stop the application (usually by pressing Ctrl+C or Esc).
|
||||
#'
|
||||
#' The host parameter was introduced in Shiny 0.9.0. Its default value of
|
||||
#' `"127.0.0.1"` means that, contrary to previous versions of Shiny, only
|
||||
#' the current machine can access locally hosted Shiny apps. To allow other
|
||||
#' clients to connect, use the value `"0.0.0.0"` instead (which was the
|
||||
#' value that was hard-coded into Shiny in 0.8.0 and earlier).
|
||||
#'
|
||||
#' @param appDir The application to run. Should be one of the following:
|
||||
#' \itemize{
|
||||
#' \item A directory containing `server.R`, plus, either `ui.R` or
|
||||
#' a `www` directory that contains the file `index.html`.
|
||||
#' \item A directory containing `app.R`.
|
||||
#' \item An `.R` file containing a Shiny application, ending with an
|
||||
#' expression that produces a Shiny app object.
|
||||
#' \item A list with `ui` and `server` components.
|
||||
#' \item A Shiny app object created by [shinyApp()].
|
||||
#' }
|
||||
#' @param port The TCP port that the application should listen on. If the
|
||||
#' `port` is not specified, and the `shiny.port` option is set (with
|
||||
#' `options(shiny.port = XX)`), then that port will be used. Otherwise,
|
||||
#' use a random port between 3000:8000, excluding ports that are blocked
|
||||
#' by Google Chrome for being considered unsafe: 3659, 4045, 5060,
|
||||
#' 5061, 6000, 6566, 6665:6669 and 6697. Up to twenty random
|
||||
#' ports will be tried.
|
||||
#' @param launch.browser If true, the system's default web browser will be
|
||||
#' launched automatically after the app is started. Defaults to true in
|
||||
#' interactive sessions only. The value of this parameter can also be a
|
||||
#' function to call with the application's URL.
|
||||
#' @param host The IPv4 address that the application should listen on. Defaults
|
||||
#' to the `shiny.host` option, if set, or `"127.0.0.1"` if not. See
|
||||
#' Details.
|
||||
#' @param workerId Can generally be ignored. Exists to help some editions of
|
||||
#' Shiny Server Pro route requests to the correct process.
|
||||
#' @param quiet Should Shiny status messages be shown? Defaults to FALSE.
|
||||
#' @param display.mode The mode in which to display the application. If set to
|
||||
#' the value `"showcase"`, shows application code and metadata from a
|
||||
#' `DESCRIPTION` file in the application directory alongside the
|
||||
#' application. If set to `"normal"`, displays the application normally.
|
||||
#' Defaults to `"auto"`, which displays the application in the mode given
|
||||
#' in its `DESCRIPTION` file, if any.
|
||||
#' @param test.mode Should the application be launched in test mode? This is
|
||||
#' only used for recording or running automated tests. Defaults to the
|
||||
#' `shiny.testmode` option, or FALSE if the option is not set.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # Start app in the current working directory
|
||||
#' runApp()
|
||||
#'
|
||||
#' # Start app in a subdirectory called myapp
|
||||
#' runApp("myapp")
|
||||
#' }
|
||||
#'
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' # Apps can be run without a server.r and ui.r file
|
||||
#' runApp(list(
|
||||
#' ui = bootstrapPage(
|
||||
#' numericInput('n', 'Number of obs', 100),
|
||||
#' plotOutput('plot')
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$plot <- renderPlot({ hist(runif(input$n)) })
|
||||
#' }
|
||||
#' ))
|
||||
#'
|
||||
#'
|
||||
#' # Running a Shiny app object
|
||||
#' app <- shinyApp(
|
||||
#' ui = bootstrapPage(
|
||||
#' numericInput('n', 'Number of obs', 100),
|
||||
#' plotOutput('plot')
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$plot <- renderPlot({ hist(runif(input$n)) })
|
||||
#' }
|
||||
#' )
|
||||
#' runApp(app)
|
||||
#' }
|
||||
#' @export
|
||||
runApp <- function(
|
||||
appDir=getwd(),
|
||||
port=getOption('shiny.port'),
|
||||
launch.browser = getOption('shiny.launch.browser', interactive()),
|
||||
host=getOption('shiny.host', '127.0.0.1'),
|
||||
workerId="", quiet=FALSE,
|
||||
display.mode=c("auto", "normal", "showcase"),
|
||||
test.mode=getOption('shiny.testmode', FALSE)
|
||||
) {
|
||||
|
||||
# * Wrap **all** execution of the app inside the otel promise domain
|
||||
# * While this could be done at a lower level, it allows for _anything_ within
|
||||
# shiny's control to allow for the opportunity to have otel active spans be
|
||||
# reactivated upon promise domain restoration
|
||||
promises::local_otel_promise_domain()
|
||||
|
||||
on.exit({
|
||||
handlerManager$clear()
|
||||
}, add = TRUE)
|
||||
|
||||
if (isRunning()) {
|
||||
stop("Can't call `runApp()` from within `runApp()`. If your ",
|
||||
"application code contains `runApp()`, please remove it.")
|
||||
}
|
||||
|
||||
# Make warnings print immediately
|
||||
# Set pool.scheduler to support pool package
|
||||
ops <- options(
|
||||
# Raise warn level to 1, but don't lower it
|
||||
warn = max(1, getOption("warn", default = 1)),
|
||||
pool.scheduler = scheduleTask
|
||||
)
|
||||
on.exit(options(ops), add = TRUE)
|
||||
|
||||
# ============================================================================
|
||||
# Global onStart/onStop callbacks
|
||||
# ============================================================================
|
||||
# Invoke user-defined onStop callbacks, before the application's internal
|
||||
# onStop callbacks.
|
||||
on.exit({
|
||||
.globals$onStopCallbacks$invoke()
|
||||
.globals$onStopCallbacks <- Callbacks$new()
|
||||
}, add = TRUE)
|
||||
|
||||
require(shiny)
|
||||
|
||||
# ============================================================================
|
||||
# Convert to Shiny app object
|
||||
# ============================================================================
|
||||
appParts <- as.shiny.appobj(appDir)
|
||||
|
||||
# ============================================================================
|
||||
# Initialize app state object
|
||||
# ============================================================================
|
||||
# This is so calls to getCurrentAppState() can be used to find (A) whether an
|
||||
# app is running and (B), get options and data associated with the app.
|
||||
initCurrentAppState(appParts)
|
||||
on.exit(clearCurrentAppState(), add = TRUE)
|
||||
# Any shinyOptions set after this point will apply to the current app only
|
||||
# (and will not persist after the app stops).
|
||||
|
||||
# ============================================================================
|
||||
# shinyOptions
|
||||
# ============================================================================
|
||||
# A unique identifier associated with this run of this application. It is
|
||||
# shared across sessions.
|
||||
shinyOptions(appToken = createUniqueId(8))
|
||||
|
||||
# Set up default cache for app.
|
||||
if (is.null(getShinyOption("cache", default = NULL))) {
|
||||
shinyOptions(cache = cachem::cache_mem(max_size = 200 * 1024^2))
|
||||
}
|
||||
|
||||
# Extract appOptions (which is a list) and store them as shinyOptions, for
|
||||
# this app. (This is the only place we have to store settings that are
|
||||
# accessible both the UI and server portion of the app.)
|
||||
applyCapturedAppOptions(appParts$appOptions)
|
||||
|
||||
# ============================================================================
|
||||
# runApp options set via shinyApp(options = list(...))
|
||||
# ============================================================================
|
||||
# The lines below set some of the app's running options, which
|
||||
# can be:
|
||||
# - left unspecified (in which case the arguments' default
|
||||
# values from `runApp` kick in);
|
||||
# - passed through `shinyApp`
|
||||
# - passed through `runApp` (this function)
|
||||
# - passed through both `shinyApp` and `runApp` (the latter
|
||||
# takes precedence)
|
||||
#
|
||||
# Matrix of possibilities:
|
||||
# | IN shinyApp | IN runApp | result | check |
|
||||
# |-------------|-----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------|
|
||||
# | no | no | use defaults | exhaust all possibilities: if it's missing (runApp does not specify); THEN if it's not in shinyApp appParts$options; THEN use defaults |
|
||||
# | yes | no | use shinyApp | if it's missing (runApp does not specify); THEN if it's in shinyApp appParts$options; THEN use shinyApp |
|
||||
# | no | yes | use runApp | if it's not missing (runApp specifies), use those |
|
||||
# | yes | yes | use runApp | if it's not missing (runApp specifies), use those |
|
||||
#
|
||||
# I tried to make this as compact and intuitive as possible,
|
||||
# given that there are four distinct possibilities to check
|
||||
appOps <- appParts$options
|
||||
findVal <- function(arg, default) {
|
||||
if (arg %in% names(appOps)) appOps[[arg]] else default
|
||||
}
|
||||
|
||||
if (missing(port))
|
||||
port <- findVal("port", port)
|
||||
if (missing(launch.browser))
|
||||
launch.browser <- findVal("launch.browser", launch.browser)
|
||||
if (missing(host))
|
||||
host <- findVal("host", host)
|
||||
if (missing(quiet))
|
||||
quiet <- findVal("quiet", quiet)
|
||||
if (missing(display.mode))
|
||||
display.mode <- findVal("display.mode", display.mode)
|
||||
if (missing(test.mode))
|
||||
test.mode <- findVal("test.mode", test.mode)
|
||||
|
||||
if (is.null(host) || is.na(host)) host <- '0.0.0.0'
|
||||
|
||||
# ============================================================================
|
||||
# Hosted environment
|
||||
# ============================================================================
|
||||
workerId(workerId)
|
||||
|
||||
if (inShinyServer()) {
|
||||
# If SHINY_PORT is set, we're running under Shiny Server. Check the version
|
||||
# to make sure it is compatible. Older versions of Shiny Server don't set
|
||||
# SHINY_SERVER_VERSION, those will return "" which is considered less than
|
||||
# any valid version.
|
||||
ver <- Sys.getenv('SHINY_SERVER_VERSION')
|
||||
if (utils::compareVersion(ver, .shinyServerMinVersion) < 0) {
|
||||
warning('Shiny Server v', .shinyServerMinVersion,
|
||||
' or later is required; please upgrade!')
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Shinytest
|
||||
# ============================================================================
|
||||
# Set the testmode shinyoption so that this can be read by both the
|
||||
# ShinySession and the UI code (which executes separately from the
|
||||
# ShinySession code).
|
||||
shinyOptions(testmode = test.mode)
|
||||
if (test.mode) {
|
||||
message("Running application in test mode.")
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Showcase mode
|
||||
# ============================================================================
|
||||
# Showcase mode is disabled by default; it must be explicitly enabled in
|
||||
# either the DESCRIPTION file for directory-based apps, or via
|
||||
# the display.mode parameter. The latter takes precedence.
|
||||
setShowcaseDefault(0)
|
||||
|
||||
# If appDir specifies a path, and display mode is specified in the
|
||||
# DESCRIPTION file at that path, apply it here.
|
||||
if (is.character(appDir)) {
|
||||
# if appDir specifies a .R file (single-file Shiny app), look for the
|
||||
# DESCRIPTION in the parent directory
|
||||
desc <- file.path.ci(
|
||||
if (tolower(tools::file_ext(appDir)) == "r")
|
||||
dirname(appDir)
|
||||
else
|
||||
appDir, "DESCRIPTION")
|
||||
if (file.exists(desc)) {
|
||||
con <- file(desc, encoding = checkEncoding(desc))
|
||||
on.exit(close(con), add = TRUE)
|
||||
settings <- read.dcf(con)
|
||||
if ("DisplayMode" %in% colnames(settings)) {
|
||||
mode <- settings[1, "DisplayMode"]
|
||||
if (mode == "Showcase") {
|
||||
setShowcaseDefault(1)
|
||||
if ("IncludeWWW" %in% colnames(settings)) {
|
||||
.globals$IncludeWWW <- as.logical(settings[1, "IncludeWWW"])
|
||||
if (is.na(.globals$IncludeWWW)) {
|
||||
stop("In your Description file, `IncludeWWW` ",
|
||||
"must be set to `True` (default) or `False`")
|
||||
}
|
||||
} else {
|
||||
.globals$IncludeWWW <- TRUE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## default is to show the .js, .css and .html files in the www directory
|
||||
## (if not in showcase mode, this variable will simply be ignored)
|
||||
if (is.null(.globals$IncludeWWW) || is.na(.globals$IncludeWWW)) {
|
||||
.globals$IncludeWWW <- TRUE
|
||||
}
|
||||
|
||||
# If display mode is specified as an argument, apply it (overriding the
|
||||
# value specified in DESCRIPTION, if any).
|
||||
display.mode <- match.arg(display.mode)
|
||||
if (display.mode == "normal") {
|
||||
setShowcaseDefault(0)
|
||||
}
|
||||
else if (display.mode == "showcase") {
|
||||
setShowcaseDefault(1)
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Server port
|
||||
# ============================================================================
|
||||
# determine port if we need to
|
||||
if (is.null(port)) {
|
||||
|
||||
# Try up to 20 random ports. If we don't succeed just plow ahead
|
||||
# with the final value we tried, and let the "real" startServer
|
||||
# somewhere down the line fail and throw the error to the user.
|
||||
#
|
||||
# If we (think we) succeed, save the value as .globals$lastPort,
|
||||
# and try that first next time the user wants a random port.
|
||||
|
||||
for (i in 1:20) {
|
||||
if (!is.null(.globals$lastPort)) {
|
||||
port <- .globals$lastPort
|
||||
.globals$lastPort <- NULL
|
||||
}
|
||||
else {
|
||||
# Try up to 20 random ports
|
||||
while (TRUE) {
|
||||
port <- p_randomInt(3000, 8000)
|
||||
# Reject ports in this range that are considered unsafe by Chrome
|
||||
# http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
|
||||
# https://github.com/rstudio/shiny/issues/1784
|
||||
# https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/net/base/port_util.cc
|
||||
if (!port %in% c(3659, 4045, 5060, 5061, 6000, 6566, 6665:6669, 6697)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Test port to see if we can use it
|
||||
tmp <- try(startServer(host, port, list()), silent=TRUE)
|
||||
if (!inherits(tmp, 'try-error')) {
|
||||
stopServer(tmp)
|
||||
.globals$lastPort <- port
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# onStart/onStop callbacks
|
||||
# ============================================================================
|
||||
# Set up the onStop before we call onStart, so that it gets called even if an
|
||||
# error happens in onStart.
|
||||
if (!is.null(appParts$onStop))
|
||||
on.exit(appParts$onStop(), add = TRUE)
|
||||
if (!is.null(appParts$onStart))
|
||||
appParts$onStart()
|
||||
|
||||
# ============================================================================
|
||||
# Start/stop httpuv app
|
||||
# ============================================================================
|
||||
server <- startApp(appParts, port, host, quiet)
|
||||
|
||||
# Make the httpuv server object accessible. Needed for calling
|
||||
# addResourcePath while app is running.
|
||||
shinyOptions(server = server)
|
||||
|
||||
on.exit({
|
||||
stopServer(server)
|
||||
}, add = TRUE)
|
||||
|
||||
# ============================================================================
|
||||
# Launch web browser
|
||||
# ============================================================================
|
||||
if (!is.character(port)) {
|
||||
browseHost <- host
|
||||
if (identical(host, "0.0.0.0")) {
|
||||
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
|
||||
browseHost <- "127.0.0.1"
|
||||
} else if (identical(host, "::")) {
|
||||
browseHost <- "::1"
|
||||
}
|
||||
|
||||
if (httpuv::ipFamily(browseHost) == 6L) {
|
||||
browseHost <- paste0("[", browseHost, "]")
|
||||
}
|
||||
|
||||
appUrl <- paste("http://", browseHost, ":", port, sep="")
|
||||
if (is.function(launch.browser))
|
||||
launch.browser(appUrl)
|
||||
else if (launch.browser)
|
||||
utils::browseURL(appUrl)
|
||||
} else {
|
||||
appUrl <- NULL
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Application hooks
|
||||
# ============================================================================
|
||||
callAppHook("onAppStart", appUrl)
|
||||
on.exit({
|
||||
callAppHook("onAppStop", appUrl)
|
||||
}, add = TRUE)
|
||||
|
||||
# ============================================================================
|
||||
# Run event loop via httpuv
|
||||
# ============================================================================
|
||||
.globals$reterror <- NULL
|
||||
.globals$retval <- NULL
|
||||
.globals$stopped <- FALSE
|
||||
# Top-level ..stacktraceoff..; matches with ..stacktraceon in observe(),
|
||||
# reactive(), Callbacks$invoke(), and others
|
||||
..stacktraceoff..(
|
||||
captureStackTraces({
|
||||
while (!.globals$stopped) {
|
||||
..stacktracefloor..(serviceApp())
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (isTRUE(.globals$reterror)) {
|
||||
stop(.globals$retval)
|
||||
}
|
||||
else if (.globals$retval$visible)
|
||||
.globals$retval$value
|
||||
else
|
||||
invisible(.globals$retval$value)
|
||||
}
|
||||
|
||||
#' Stop the currently running Shiny app
|
||||
#'
|
||||
#' Stops the currently running Shiny app, returning control to the caller of
|
||||
#' [runApp()].
|
||||
#'
|
||||
#' @param returnValue The value that should be returned from
|
||||
#' [runApp()].
|
||||
#' @export
|
||||
stopApp <- function(returnValue = invisible()) {
|
||||
# reterror will indicate whether retval is an error (i.e. it should be passed
|
||||
# to stop() when the serviceApp loop stops) or a regular value (in which case
|
||||
# it should simply be returned with the appropriate visibility).
|
||||
.globals$reterror <- FALSE
|
||||
..stacktraceoff..(
|
||||
tryCatch(
|
||||
{
|
||||
captureStackTraces(
|
||||
.globals$retval <- withVisible(..stacktraceon..(force(returnValue)))
|
||||
)
|
||||
},
|
||||
error = function(e) {
|
||||
.globals$retval <- e
|
||||
.globals$reterror <- TRUE
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
.globals$stopped <- TRUE
|
||||
httpuv::interrupt()
|
||||
}
|
||||
|
||||
#' Run Shiny Example Applications
|
||||
#'
|
||||
#' Launch Shiny example applications, and optionally, your system's web browser.
|
||||
#'
|
||||
#' @param example The name of the example to run, or `NA` (the default) to
|
||||
#' list the available examples.
|
||||
#' @param launch.browser If true, the system's default web browser will be
|
||||
#' launched automatically after the app is started. Defaults to true in
|
||||
#' interactive sessions only.
|
||||
#' @param host The IPv4 address that the application should listen on. Defaults
|
||||
#' to the `shiny.host` option, if set, or `"127.0.0.1"` if not.
|
||||
#' @param display.mode The mode in which to display the example. Defaults to
|
||||
#' `"auto"`, which uses the value of `DisplayMode` in the example's
|
||||
#' `DESCRIPTION` file. Set to `"showcase"` to show the app code and
|
||||
#' description with the running app, or `"normal"` to see the example without
|
||||
#' code or commentary.
|
||||
#' @param package The package in which to find the example (defaults to
|
||||
#' `"shiny"`).
|
||||
#'
|
||||
#' To provide examples in your package, store examples in the
|
||||
#' `inst/examples-shiny` directory of your package. Each example should be
|
||||
#' in its own subdirectory and should be runnable when [runApp()] is called
|
||||
#' on the subdirectory. Example apps can include a `DESCRIPTION` file and a
|
||||
#' `README.md` file to provide metadata and commentary about the example. See
|
||||
#' the article on [Display Modes](https://shiny.posit.co/r/articles/build/display-modes/)
|
||||
#' on the Shiny website for more information.
|
||||
#' @inheritParams runApp
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' # List all available examples
|
||||
#' runExample()
|
||||
#'
|
||||
#' # Run one of the examples
|
||||
#' runExample("01_hello")
|
||||
#'
|
||||
#' # Print the directory containing the code for all examples
|
||||
#' system.file("examples", package="shiny")
|
||||
#' }
|
||||
#' @export
|
||||
runExample <- function(
|
||||
example = NA,
|
||||
port = getOption("shiny.port"),
|
||||
launch.browser = getOption("shiny.launch.browser", interactive()),
|
||||
host = getOption("shiny.host", "127.0.0.1"),
|
||||
display.mode = c("auto", "normal", "showcase"),
|
||||
package = "shiny"
|
||||
) {
|
||||
if (!identical(package, "shiny") && !is_installed(package)) {
|
||||
rlang::check_installed(package)
|
||||
}
|
||||
|
||||
use_legacy_shiny_examples <-
|
||||
identical(package, "shiny") &&
|
||||
isTRUE(getOption('shiny.legacy.examples', FALSE))
|
||||
|
||||
examplesDir <- system_file(
|
||||
if (use_legacy_shiny_examples) "examples" else "examples-shiny",
|
||||
package = package
|
||||
)
|
||||
|
||||
dir <- resolve(examplesDir, example)
|
||||
|
||||
if (is.null(dir)) {
|
||||
valid_examples <- sprintf(
|
||||
'Valid examples in {%s}: "%s"',
|
||||
package,
|
||||
paste(list.files(examplesDir), collapse = '", "')
|
||||
)
|
||||
|
||||
if (is.na(example)) {
|
||||
message(valid_examples)
|
||||
return(invisible())
|
||||
}
|
||||
|
||||
stop("Example '", example, "' does not exist. ", valid_examples)
|
||||
}
|
||||
|
||||
runApp(dir, port = port, host = host, launch.browser = launch.browser,
|
||||
display.mode = display.mode)
|
||||
}
|
||||
|
||||
#' Run a gadget
|
||||
#'
|
||||
#' Similar to `runApp`, but handles `input$cancel` automatically, and
|
||||
#' if running in RStudio, defaults to viewing the app in the Viewer pane.
|
||||
#'
|
||||
#' @param app Either a Shiny app object as created by
|
||||
#' [`shinyApp()`][shiny] et al, or, a UI object.
|
||||
#' @param server Ignored if `app` is a Shiny app object; otherwise, passed
|
||||
#' along to `shinyApp` (i.e. `shinyApp(ui = app, server = server)`).
|
||||
#' @param port See [`runApp()`][shiny].
|
||||
#' @param viewer Specify where the gadget should be displayed--viewer pane,
|
||||
#' dialog window, or external browser--by passing in a call to one of the
|
||||
#' [viewer()] functions.
|
||||
#' @param stopOnCancel If `TRUE` (the default), then an `observeEvent`
|
||||
#' is automatically created that handles `input$cancel` by calling
|
||||
#' `stopApp()` with an error. Pass `FALSE` if you want to handle
|
||||
#' `input$cancel` yourself.
|
||||
#' @return The value returned by the gadget.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' library(shiny)
|
||||
#'
|
||||
#' ui <- fillPage(...)
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' ...
|
||||
#' }
|
||||
#'
|
||||
#' # Either pass ui/server as separate arguments...
|
||||
#' runGadget(ui, server)
|
||||
#'
|
||||
#' # ...or as a single app object
|
||||
#' runGadget(shinyApp(ui, server))
|
||||
#' }
|
||||
#' @export
|
||||
runGadget <- function(app, server = NULL, port = getOption("shiny.port"),
|
||||
viewer = paneViewer(), stopOnCancel = TRUE) {
|
||||
|
||||
if (!is.shiny.appobj(app)) {
|
||||
app <- shinyApp(app, server)
|
||||
}
|
||||
|
||||
if (isTRUE(stopOnCancel)) {
|
||||
app <- decorateServerFunc(app, function(input, output, session) {
|
||||
observeEvent(input$cancel, {
|
||||
stopApp(stop("User cancel", call. = FALSE))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (is.null(viewer)) {
|
||||
viewer <- utils::browseURL
|
||||
}
|
||||
|
||||
shiny::runApp(app, port = port, launch.browser = viewer)
|
||||
}
|
||||
|
||||
# Add custom functionality to a Shiny app object's server func
|
||||
decorateServerFunc <- function(appobj, serverFunc) {
|
||||
origServerFuncSource <- appobj$serverFuncSource
|
||||
appobj$serverFuncSource <- function() {
|
||||
origServerFunc <- origServerFuncSource()
|
||||
function(input, output, session) {
|
||||
serverFunc(input, output, session)
|
||||
|
||||
# The clientData and session arguments are optional; check if
|
||||
# each exists
|
||||
args <- argsForServerFunc(origServerFunc, session)
|
||||
do.call(origServerFunc, args)
|
||||
}
|
||||
}
|
||||
appobj
|
||||
}
|
||||
90
R/serializers.R
Normal file
90
R/serializers.R
Normal file
@@ -0,0 +1,90 @@
|
||||
#' Add a function for serializing an input before bookmarking application state
|
||||
#'
|
||||
#' @param inputId Name of the input value.
|
||||
#' @param fun A function that takes the input value and returns a modified
|
||||
#' value. The returned value will be used for the test snapshot.
|
||||
#' @param session A Shiny session object.
|
||||
#'
|
||||
#' @export
|
||||
setSerializer <- function(inputId, fun, session = getDefaultReactiveDomain()) {
|
||||
if (is.null(session)) {
|
||||
stop("setSerializer() needs a session object.")
|
||||
}
|
||||
|
||||
input_impl <- .subset2(session$input, "impl")
|
||||
input_impl$setMeta(inputId, "shiny.serializer", fun)
|
||||
}
|
||||
|
||||
|
||||
# For most types of values, simply return the value unchanged.
|
||||
serializerDefault <- function(value, stateDir) {
|
||||
value
|
||||
}
|
||||
|
||||
|
||||
serializerFileInput <- function(value, stateDir = NULL) {
|
||||
# File inputs can be serialized only if there's a stateDir
|
||||
if (is.null(stateDir)) {
|
||||
return(serializerUnserializable())
|
||||
}
|
||||
|
||||
# value is a data frame. When persisting files, we need to copy the file to
|
||||
# the persistent dir and then strip the original path before saving.
|
||||
newpaths <- file.path(stateDir, basename(value$datapath))
|
||||
file.copy(value$datapath, newpaths, overwrite = TRUE)
|
||||
value$datapath <- basename(newpaths)
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
|
||||
# Return a sentinel value that represents "unserializable". This is applied to
|
||||
# for example, passwords and actionButtons.
|
||||
serializerUnserializable <- function(value, stateDir) {
|
||||
structure(
|
||||
list(),
|
||||
serializable = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
# Is this an "unserializable" sentinel value?
|
||||
isUnserializable <- function(x) {
|
||||
identical(
|
||||
attr(x, "serializable", exact = TRUE),
|
||||
FALSE
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# Given a reactiveValues object and optional directory for saving state, apply
|
||||
# serializer function to each of the values, and return a list of the returned
|
||||
# values. This function passes stateDir to the serializer functions, so if
|
||||
# stateDir is non-NULL, it can have a side effect of writing values to disk (in
|
||||
# stateDir).
|
||||
serializeReactiveValues <- function(values, exclude, stateDir = NULL) {
|
||||
impl <- .subset2(values, "impl")
|
||||
|
||||
# Get named list where keys and values are the names of inputs; we'll retrieve
|
||||
# actual values later.
|
||||
vals <- isolate(impl$names())
|
||||
vals <- setdiff(vals, exclude)
|
||||
names(vals) <- vals
|
||||
|
||||
# Get values and apply serializer functions
|
||||
vals <- lapply(vals, function(name) {
|
||||
val <- impl$get(name)
|
||||
|
||||
# Get the serializer function for this input value. If none specified, use
|
||||
# the default.
|
||||
serializer_fun <- impl$getMeta(name, "shiny.serializer")
|
||||
if (is.null(serializer_fun))
|
||||
serializer_fun <- serializerDefault
|
||||
|
||||
# Apply serializer function.
|
||||
serializer_fun(val, stateDir)
|
||||
})
|
||||
|
||||
# Filter out any values that were marked as unserializable.
|
||||
vals <- Filter(Negate(isUnserializable), vals)
|
||||
vals
|
||||
}
|
||||
245
R/server-input-handlers.R
Normal file
245
R/server-input-handlers.R
Normal file
@@ -0,0 +1,245 @@
|
||||
# Create a Map object for input handlers and register the defaults.
|
||||
# This is assigned in .onLoad time.
|
||||
inputHandlers <- NULL
|
||||
on_load({
|
||||
inputHandlers <- Map$new()
|
||||
})
|
||||
|
||||
#' Register an Input Handler
|
||||
#'
|
||||
#' Adds an input handler for data of this type. When called, Shiny will use the
|
||||
#' function provided to refine the data passed back from the client (after being
|
||||
#' deserialized by jsonlite) before making it available in the `input` variable
|
||||
#' of the `server.R` file.
|
||||
#'
|
||||
#' This function will register the handler for the duration of the R process
|
||||
#' (unless Shiny is explicitly reloaded). For that reason, the `type` used
|
||||
#' should be very specific to this package to minimize the risk of colliding
|
||||
#' with another Shiny package which might use this data type name. We recommend
|
||||
#' the format of "packageName.widgetName". It should be called from the
|
||||
#' package's `.onLoad()` function.
|
||||
#'
|
||||
#' Currently Shiny registers the following handlers: `shiny.matrix`,
|
||||
#' `shiny.number`, and `shiny.date`.
|
||||
#'
|
||||
#' The `type` of a custom Shiny Input widget will be deduced using the
|
||||
#' `getType()` JavaScript function on the registered Shiny inputBinding.
|
||||
#' @param type The type for which the handler should be added --- should be a
|
||||
#' single-element character vector.
|
||||
#' @param fun The handler function. This is the function that will be used to
|
||||
#' parse the data delivered from the client before it is available in the
|
||||
#' `input` variable. The function will be called with the following three
|
||||
#' parameters: \enumerate{ \item{The value of this input as provided by the
|
||||
#' client, deserialized using jsonlite.} \item{The `shinysession` in which the
|
||||
#' input exists.} \item{The name of the input.} }
|
||||
#' @param force If `TRUE`, will overwrite any existing handler without warning.
|
||||
#' If `FALSE`, will throw an error if this class already has a handler
|
||||
#' defined.
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # Register an input handler which rounds a input number to the nearest integer
|
||||
#' # In a package, this should be called from the .onLoad function.
|
||||
#' registerInputHandler("mypackage.validint", function(x, shinysession, name) {
|
||||
#' if (is.null(x)) return(NA)
|
||||
#' round(x)
|
||||
#' })
|
||||
#'
|
||||
#' ## On the Javascript side, the associated input binding must have a corresponding getType method:
|
||||
#' # getType: function(el) {
|
||||
#' # return "mypackage.validint";
|
||||
#' # }
|
||||
#'
|
||||
#' }
|
||||
#' @seealso [removeInputHandler()] [applyInputHandlers()]
|
||||
#' @export
|
||||
registerInputHandler <- function(type, fun, force=FALSE){
|
||||
if (inputHandlers$containsKey(type) && !force){
|
||||
stop("There is already an input handler for type: ", type)
|
||||
}
|
||||
inputHandlers$set(type, fun)
|
||||
}
|
||||
|
||||
#' Deregister an Input Handler
|
||||
#'
|
||||
#' Removes an Input Handler. Rather than using the previously specified handler
|
||||
#' for data of this type, the default jsonlite serialization will be used.
|
||||
#'
|
||||
#' @param type The type for which handlers should be removed.
|
||||
#' @return The handler previously associated with this `type`, if one
|
||||
#' existed. Otherwise, `NULL`.
|
||||
#' @seealso [registerInputHandler()]
|
||||
#' @export
|
||||
removeInputHandler <- function(type){
|
||||
inputHandlers$remove(type)
|
||||
}
|
||||
|
||||
|
||||
# Apply input handler to a single input value
|
||||
applyInputHandler <- function(name, val, shinysession) {
|
||||
splitName <- strsplit(name, ':')[[1]]
|
||||
if (length(splitName) > 1) {
|
||||
if (!inputHandlers$containsKey(splitName[[2]])) {
|
||||
# No input handler registered for this type
|
||||
stop("No handler registered for type ", name)
|
||||
}
|
||||
|
||||
inputName <- splitName[[1]]
|
||||
|
||||
# Get the function for processing this type of input
|
||||
inputHandler <- inputHandlers$get(splitName[[2]])
|
||||
|
||||
return(inputHandler(val, shinysession, inputName))
|
||||
|
||||
} else if (is.list(val) && is.null(names(val))) {
|
||||
return(unlist(val, recursive = TRUE))
|
||||
} else {
|
||||
return(val)
|
||||
}
|
||||
}
|
||||
|
||||
#' Apply input handlers to raw input values
|
||||
#'
|
||||
#' The purpose of this function is to make it possible for external packages to
|
||||
#' test Shiny inputs. It takes a named list of raw input values, applies input
|
||||
#' handlers to those values, and then returns a named list of the processed
|
||||
#' values.
|
||||
#'
|
||||
#' The raw input values should be in a named list. Some values may have names
|
||||
#' like `"x:shiny.date"`. This function would apply the `"shiny.date"`
|
||||
#' input handler to the value, and then rename the result to `"x"`, in the
|
||||
#' output.
|
||||
#'
|
||||
#' @param inputs A named list of input values.
|
||||
#' @param shinysession A Shiny session object.
|
||||
#'
|
||||
#' @seealso registerInputHandler
|
||||
#' @keywords internal
|
||||
applyInputHandlers <- function(inputs, shinysession = getDefaultReactiveDomain()) {
|
||||
inputs <- mapply(applyInputHandler, names(inputs), inputs,
|
||||
MoreArgs = list(shinysession = shinysession),
|
||||
SIMPLIFY = FALSE)
|
||||
|
||||
# Convert names like "button1:shiny.action" to "button1"
|
||||
names(inputs) <- vapply(
|
||||
names(inputs),
|
||||
function(name) { strsplit(name, ":")[[1]][1] },
|
||||
FUN.VALUE = character(1)
|
||||
)
|
||||
|
||||
inputs
|
||||
}
|
||||
|
||||
on_load({
|
||||
# Takes a list-of-lists and returns a matrix. The lists
|
||||
# must all be the same length. NULL is replaced by NA.
|
||||
registerInputHandler("shiny.matrix", function(data, ...) {
|
||||
if (length(data) == 0)
|
||||
return(matrix(nrow=0, ncol=0))
|
||||
|
||||
m <- matrix(unlist(lapply(data, function(x) {
|
||||
sapply(x, function(y) {
|
||||
ifelse(is.null(y), NA, y)
|
||||
})
|
||||
})), nrow = length(data[[1]]), ncol = length(data))
|
||||
return(m)
|
||||
})
|
||||
|
||||
|
||||
registerInputHandler("shiny.number", function(val, ...){
|
||||
ifelse(is.null(val), NA, val)
|
||||
})
|
||||
|
||||
registerInputHandler("shiny.password", function(val, shinysession, name) {
|
||||
# Mark passwords as not serializable
|
||||
setSerializer(name, serializerUnserializable)
|
||||
val
|
||||
})
|
||||
|
||||
registerInputHandler("shiny.date", function(val, ...){
|
||||
# First replace NULLs with NA, then convert to Date vector
|
||||
datelist <- ifelse(lapply(val, is.null), NA, val)
|
||||
|
||||
res <- NULL
|
||||
tryCatch({
|
||||
res <- as.Date(unlist(datelist))
|
||||
},
|
||||
error = function(e) {
|
||||
# It's possible for client to send a string like "99999-01-01", which
|
||||
# as.Date can't handle.
|
||||
warning(e$message)
|
||||
res <<- as.Date(rep(NA, length(datelist)))
|
||||
}
|
||||
)
|
||||
|
||||
res
|
||||
})
|
||||
|
||||
registerInputHandler("shiny.datetime", function(val, ...){
|
||||
# First replace NULLs with NA, then convert to POSIXct vector
|
||||
times <- lapply(val, function(x) {
|
||||
if (is.null(x)) NA
|
||||
else x
|
||||
})
|
||||
as.POSIXct(unlist(times), origin = "1970-01-01", tz = "UTC")
|
||||
})
|
||||
|
||||
registerInputHandler("shiny.action", function(val, shinysession, name) {
|
||||
# mark up the action button value with a special class so we can recognize it later
|
||||
class(val) <- c("shinyActionButtonValue", class(val))
|
||||
val
|
||||
})
|
||||
|
||||
registerInputHandler("shiny.file", function(val, shinysession, name) {
|
||||
# This function is only used when restoring a Shiny fileInput. When a file is
|
||||
# uploaded the usual way, it takes a different code path and won't hit this
|
||||
# function.
|
||||
if (is.null(val))
|
||||
return(NULL)
|
||||
|
||||
# The data will be a named list of lists; convert to a data frame.
|
||||
val <- as.data.frame(lapply(val, unlist), stringsAsFactors = FALSE)
|
||||
|
||||
# `val$datapath` should be a filename without a path, for security reasons.
|
||||
if (basename(val$datapath) != val$datapath) {
|
||||
stop("Invalid '/' found in file input path.")
|
||||
}
|
||||
|
||||
# Prepend the persistent dir
|
||||
oldfile <- file.path(getCurrentRestoreContext()$dir, val$datapath)
|
||||
|
||||
# Copy the original file to a new temp dir, so that a restored session can't
|
||||
# modify the original.
|
||||
newdir <- file.path(tempdir(), createUniqueId(12))
|
||||
dir.create(newdir)
|
||||
val$datapath <- file.path(newdir, val$datapath)
|
||||
file.copy(oldfile, val$datapath)
|
||||
|
||||
# Need to mark this input value with the correct serializer. When a file is
|
||||
# uploaded the usual way (instead of being restored), this occurs in
|
||||
# session$`@uploadEnd`.
|
||||
setSerializer(name, serializerFileInput)
|
||||
|
||||
snapshotPreprocessInput(name, snapshotPreprocessorFileInput)
|
||||
|
||||
val
|
||||
})
|
||||
|
||||
|
||||
# to be used with !!!answer
|
||||
registerInputHandler("shiny.symbolList", function(val, ...) {
|
||||
if (is.null(val)) {
|
||||
list()
|
||||
} else {
|
||||
lapply(val, as.symbol)
|
||||
}
|
||||
})
|
||||
# to be used with !!answer
|
||||
registerInputHandler("shiny.symbol", function(val, ...) {
|
||||
if (is.null(val) || identical(val, "")) {
|
||||
NULL
|
||||
} else {
|
||||
as.symbol(val)
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
169
R/server-resource-paths.R
Normal file
169
R/server-resource-paths.R
Normal file
@@ -0,0 +1,169 @@
|
||||
.globals$resourcePaths <- list()
|
||||
.globals$resources <- list()
|
||||
|
||||
#' Resource Publishing
|
||||
#'
|
||||
#' Add, remove, or list directory of static resources to Shiny's web server,
|
||||
#' with the given path prefix. Primarily intended for package authors to make
|
||||
#' supporting JavaScript/CSS files available to their components.
|
||||
#'
|
||||
#' Shiny provides two ways of serving static files (i.e., resources):
|
||||
#'
|
||||
#' 1. Static files under the `www/` directory are automatically made available
|
||||
#' under a request path that begins with `/`.
|
||||
#' 2. `addResourcePath()` makes static files in a `directoryPath` available
|
||||
#' under a request path that begins with `prefix`.
|
||||
#'
|
||||
#' The second approach is primarily intended for package authors to make
|
||||
#' supporting JavaScript/CSS files available to their components.
|
||||
#'
|
||||
#' Tools for managing static resources published by Shiny's web server:
|
||||
#' * `addResourcePath()` adds a directory of static resources.
|
||||
#' * `resourcePaths()` lists the currently active resource mappings.
|
||||
#' * `removeResourcePath()` removes a directory of static resources.
|
||||
#'
|
||||
#' @param prefix The URL prefix (without slashes). Valid characters are a-z,
|
||||
#' A-Z, 0-9, hyphen, period, and underscore. For example, a value of 'foo'
|
||||
#' means that any request paths that begin with '/foo' will be mapped to the
|
||||
#' given directory.
|
||||
#' @param directoryPath The directory that contains the static resources to be
|
||||
#' served.
|
||||
#'
|
||||
#' @rdname resourcePaths
|
||||
#' @seealso [singleton()]
|
||||
#'
|
||||
#' @examples
|
||||
#' addResourcePath('datasets', system.file('data', package='datasets'))
|
||||
#' resourcePaths()
|
||||
#' removeResourcePath('datasets')
|
||||
#' resourcePaths()
|
||||
#'
|
||||
#' # make sure all resources are removed
|
||||
#' lapply(names(resourcePaths()), removeResourcePath)
|
||||
#' @export
|
||||
addResourcePath <- function(prefix, directoryPath) {
|
||||
if (length(prefix) != 1) stop("prefix must be of length 1")
|
||||
if (grepl("^\\.+$", prefix)) stop("prefix can't be composed of dots only")
|
||||
if (!grepl('[a-z0-9\\-_.]+$', prefix, ignore.case = TRUE, perl = TRUE)) {
|
||||
stop("addResourcePath called with invalid prefix; please see documentation")
|
||||
}
|
||||
if (prefix %in% c('shared')) {
|
||||
stop("addResourcePath called with the reserved prefix '", prefix, "'; ",
|
||||
"please use a different prefix")
|
||||
}
|
||||
normalizedPath <- tryCatch(normalizePath(directoryPath, mustWork = TRUE),
|
||||
error = function(e) {
|
||||
stop("Couldn't normalize path in `addResourcePath`, with arguments: ",
|
||||
"`prefix` = '", prefix, "'; `directoryPath` = '" , directoryPath, "'")
|
||||
}
|
||||
)
|
||||
|
||||
# # Often times overwriting a resource path is "what you want",
|
||||
# # but sometimes it can lead to difficult to diagnose issues
|
||||
# # (e.g. an implict dependency might set a resource path that
|
||||
# # conflicts with what you, the app author, are trying to register)
|
||||
# # Note that previous versions of shiny used to warn about this case,
|
||||
# # but it was eventually removed since it caused confusion (#567).
|
||||
# # It seems a good compromise is to throw a more information message.
|
||||
# if (getOption("shiny.resourcePathChanges", FALSE) &&
|
||||
# prefix %in% names(.globals$resourcePaths)) {
|
||||
# existingPath <- .globals$resourcePaths[[prefix]]$path
|
||||
# if (normalizedPath != existingPath) {
|
||||
# message(
|
||||
# "The resource path '", prefix, "' used to point to ",
|
||||
# existingPath, ", but it now points to ", normalizedPath, ". ",
|
||||
# "If your app doesn't work as expected, you may want to ",
|
||||
# "choose a different prefix name."
|
||||
# )
|
||||
# }
|
||||
# }
|
||||
|
||||
# If a shiny app is currently running, dynamically register this path with
|
||||
# the corresponding httpuv server object.
|
||||
if (!is.null(getShinyOption("server", default = NULL)))
|
||||
{
|
||||
getShinyOption("server")$setStaticPath(.list = stats::setNames(normalizedPath, prefix))
|
||||
}
|
||||
|
||||
# .globals$resourcePaths and .globals$resources persist across runs of applications.
|
||||
.globals$resourcePaths[[prefix]] <- staticPath(normalizedPath)
|
||||
# This is necessary because resourcePaths is only for serving assets out of C++;
|
||||
# to support subapps, we also need assets to be served out of R, because those
|
||||
# URLs are rewritten by R code (i.e. routeHandler) before they can be matched to
|
||||
# a resource path.
|
||||
.globals$resources[[prefix]] <- list(
|
||||
directoryPath = normalizedPath,
|
||||
func = staticHandler(normalizedPath)
|
||||
)
|
||||
}
|
||||
|
||||
#' @rdname resourcePaths
|
||||
#' @export
|
||||
resourcePaths <- function() {
|
||||
urls <- names(.globals$resourcePaths)
|
||||
paths <- vapply(.globals$resourcePaths, function(x) x$path, character(1))
|
||||
stats::setNames(paths, urls)
|
||||
}
|
||||
|
||||
hasResourcePath <- function(prefix) {
|
||||
prefix %in% names(resourcePaths())
|
||||
}
|
||||
|
||||
#' @rdname resourcePaths
|
||||
#' @export
|
||||
removeResourcePath <- function(prefix) {
|
||||
if (length(prefix) > 1) stop("`prefix` must be of length 1.")
|
||||
if (!hasResourcePath(prefix)) {
|
||||
warning("Resource ", prefix, " not found.")
|
||||
return(invisible(FALSE))
|
||||
}
|
||||
.globals$resourcePaths[[prefix]] <- NULL
|
||||
.globals$resources[[prefix]] <- NULL
|
||||
invisible(TRUE)
|
||||
}
|
||||
|
||||
# This function handles any GET request with two or more path elements where the
|
||||
# first path element matches a prefix that was previously added using
|
||||
# addResourcePath().
|
||||
#
|
||||
# For example, if `addResourcePath("foo", "~/bar")` was called, then a GET
|
||||
# request for /foo/one/two.html would rewrite the PATH_INFO as /one/two.html and
|
||||
# send it to the resource path function for "foo". As of this writing, that
|
||||
# function will always be a staticHandler, which serves up a file if it exists
|
||||
# and NULL if it does not.
|
||||
#
|
||||
# Since Shiny 1.3.x, assets registered via addResourcePath should mostly be
|
||||
# served out of httpuv's native static file serving features. However, in the
|
||||
# specific case of subapps, the R code path must be used, because subapps insert
|
||||
# a giant random ID into the beginning of the URL that must be stripped off by
|
||||
# an R route handler (see addSubApp()).
|
||||
resourcePathHandler <- function(req) {
|
||||
if (!identical(req$REQUEST_METHOD, 'GET'))
|
||||
return(NULL)
|
||||
|
||||
# e.g. "/foo/one/two.html"
|
||||
path <- req$PATH_INFO
|
||||
|
||||
match <- regexpr('^/([^/]+)/', path, perl=TRUE)
|
||||
if (match == -1)
|
||||
return(NULL)
|
||||
len <- attr(match, 'capture.length')
|
||||
# e.g. "foo"
|
||||
prefix <- substr(path, 2, 2 + len - 1)
|
||||
|
||||
resInfo <- .globals$resources[[prefix]]
|
||||
if (is.null(resInfo))
|
||||
return(NULL)
|
||||
|
||||
# e.g. "/one/two.html"
|
||||
suffix <- substr(path, 2 + len, nchar(path))
|
||||
|
||||
# Create a new request that's a clone of the current request, but adjust
|
||||
# PATH_INFO and SCRIPT_NAME to reflect that we have already matched the first
|
||||
# path element (e.g. "/foo"). See routeHandler() for more info.
|
||||
subreq <- as.environment(as.list(req, all.names=TRUE))
|
||||
subreq$PATH_INFO <- suffix
|
||||
subreq$SCRIPT_NAME <- paste(subreq$SCRIPT_NAME, substr(path, 1, 2 + len), sep='')
|
||||
|
||||
return(resInfo$func(subreq))
|
||||
}
|
||||
525
R/server.R
Normal file
525
R/server.R
Normal file
@@ -0,0 +1,525 @@
|
||||
#' @include server-input-handlers.R
|
||||
|
||||
appsByToken <- NULL
|
||||
appsNeedingFlush <- NULL
|
||||
on_load({
|
||||
appsByToken <- Map$new()
|
||||
appsNeedingFlush <- Map$new()
|
||||
})
|
||||
|
||||
|
||||
# Provide a character representation of the WS that can be used
|
||||
# as a key in a Map.
|
||||
wsToKey <- function(WS) {
|
||||
as.character(WS$socket)
|
||||
}
|
||||
|
||||
.globals$clients <- function(req) NULL
|
||||
|
||||
|
||||
clearClients <- function() {
|
||||
.globals$clients <- function(req) NULL
|
||||
}
|
||||
|
||||
|
||||
registerClient <- function(client) {
|
||||
.globals$clients <- append(.globals$clients, client)
|
||||
}
|
||||
|
||||
|
||||
.globals$showcaseDefault <- 0
|
||||
|
||||
.globals$showcaseOverride <- FALSE
|
||||
|
||||
|
||||
#' Define Server Functionality
|
||||
#'
|
||||
#' @description `r lifecycle::badge("superseded")`
|
||||
#'
|
||||
#' @description Defines the server-side logic of the Shiny application. This generally
|
||||
#' involves creating functions that map user inputs to various kinds of output.
|
||||
#' In older versions of Shiny, it was necessary to call `shinyServer()` in
|
||||
#' the `server.R` file, but this is no longer required as of Shiny 0.10.
|
||||
#' Now the `server.R` file may simply return the appropriate server
|
||||
#' function (as the last expression in the code), without calling
|
||||
#' `shinyServer()`.
|
||||
#'
|
||||
#' Call `shinyServer` from your application's `server.R`
|
||||
#' file, passing in a "server function" that provides the server-side logic of
|
||||
#' your application.
|
||||
#'
|
||||
#' The server function will be called when each client (web browser) first loads
|
||||
#' the Shiny application's page. It must take an `input` and an
|
||||
#' `output` parameter. Any return value will be ignored. It also takes an
|
||||
#' optional `session` parameter, which is used when greater control is
|
||||
#' needed.
|
||||
#'
|
||||
#' See the [tutorial](https://shiny.rstudio.com/tutorial/) for more
|
||||
#' on how to write a server function.
|
||||
#'
|
||||
#' @param func The server function for this application. See the details section
|
||||
#' for more information.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # A very simple Shiny app that takes a message from the user
|
||||
#' # and outputs an uppercase version of it.
|
||||
#' shinyServer(function(input, output, session) {
|
||||
#' output$uppercase <- renderText({
|
||||
#' toupper(input$message)
|
||||
#' })
|
||||
#' })
|
||||
#'
|
||||
#'
|
||||
#' # It is also possible for a server.R file to simply return the function,
|
||||
#' # without calling shinyServer().
|
||||
#' # For example, the server.R file could contain just the following:
|
||||
#' function(input, output, session) {
|
||||
#' output$uppercase <- renderText({
|
||||
#' toupper(input$message)
|
||||
#' })
|
||||
#' }
|
||||
#' }
|
||||
#' @export
|
||||
#' @keywords internal
|
||||
shinyServer <- function(func) {
|
||||
if (in_devmode()) {
|
||||
shinyDeprecated(
|
||||
"0.10.0", "shinyServer()",
|
||||
details = paste0(
|
||||
"When removing `shinyServer()`, ",
|
||||
"ensure that the last expression returned from server.R ",
|
||||
"is the function normally supplied to `shinyServer(func)`."
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
.globals$server <- list(func)
|
||||
invisible(func)
|
||||
}
|
||||
|
||||
decodeMessage <- function(data) {
|
||||
readInt <- function(pos) {
|
||||
packBits(rawToBits(data[pos:(pos+3)]), type='integer')
|
||||
}
|
||||
|
||||
if (readInt(1) != 0x01020202L) {
|
||||
# Treat message as UTF-8
|
||||
charData <- rawToChar(data)
|
||||
Encoding(charData) <- 'UTF-8'
|
||||
return(safeFromJSON(charData, simplifyVector=FALSE))
|
||||
}
|
||||
|
||||
i <- 5
|
||||
parts <- list()
|
||||
while (i <= length(data)) {
|
||||
length <- readInt(i)
|
||||
i <- i + 4
|
||||
if (length != 0)
|
||||
parts <- append(parts, list(data[i:(i+length-1)]))
|
||||
else
|
||||
parts <- append(parts, list(raw(0)))
|
||||
i <- i + length
|
||||
}
|
||||
|
||||
mainMessage <- decodeMessage(parts[[1]])
|
||||
mainMessage$blobs <- parts[2:length(parts)]
|
||||
return(mainMessage)
|
||||
}
|
||||
|
||||
autoReloadCallbacks <- NULL
|
||||
on_load({
|
||||
autoReloadCallbacks <- Callbacks$new()
|
||||
})
|
||||
|
||||
createAppHandlers <- function(httpHandlers, serverFuncSource) {
|
||||
appvars <- new.env()
|
||||
appvars$server <- NULL
|
||||
|
||||
sys.www.root <- system_file('www', package='shiny')
|
||||
|
||||
# This value, if non-NULL, must be present on all HTTP and WebSocket
|
||||
# requests as the Shiny-Shared-Secret header or else access will be
|
||||
# denied (403 response for HTTP, and instant close for websocket).
|
||||
checkSharedSecret <- loadSharedSecret()
|
||||
|
||||
appHandlers <- list(
|
||||
http = joinHandlers(c(
|
||||
sessionHandler,
|
||||
httpHandlers,
|
||||
sys.www.root,
|
||||
resourcePathHandler,
|
||||
reactLogHandler
|
||||
)),
|
||||
ws = function(ws) {
|
||||
if (!checkSharedSecret(ws$request$HTTP_SHINY_SHARED_SECRET)) {
|
||||
ws$close()
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
if (identical(ws$request$PATH_INFO, "/autoreload/")) {
|
||||
if (!get_devmode_option("shiny.autoreload", FALSE)) {
|
||||
ws$close()
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
callbackHandle <- autoReloadCallbacks$register(function() {
|
||||
ws$send("autoreload")
|
||||
ws$close()
|
||||
})
|
||||
ws$onClose(function() {
|
||||
callbackHandle()
|
||||
})
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
if (!is.null(getOption("shiny.observer.error", NULL))) {
|
||||
warning(
|
||||
call. = FALSE,
|
||||
"options(shiny.observer.error) is no longer supported; please unset it!"
|
||||
)
|
||||
stopApp()
|
||||
}
|
||||
|
||||
shinysession <- ShinySession$new(ws)
|
||||
appsByToken$set(shinysession$token, shinysession)
|
||||
shinysession$setShowcase(.globals$showcaseDefault)
|
||||
|
||||
messageHandler <- function(binary, msg) {
|
||||
withReactiveDomain(shinysession, {
|
||||
# To ease transition from websockets-based code. Should remove once we're stable.
|
||||
if (is.character(msg))
|
||||
msg <- charToRaw(msg)
|
||||
|
||||
traceOption <- getOption('shiny.trace', FALSE)
|
||||
if (isTRUE(traceOption) || traceOption == "recv") {
|
||||
if (binary)
|
||||
message("RECV ", '$$binary data$$')
|
||||
else
|
||||
message("RECV ", rawToChar(msg))
|
||||
}
|
||||
|
||||
if (isEmptyMessage(msg))
|
||||
return()
|
||||
|
||||
msg <- decodeMessage(msg)
|
||||
|
||||
# Set up a restore context from .clientdata_url_search before
|
||||
# handling all the input values, because the restore context may be
|
||||
# used by an input handler (like the one for "shiny.file"). This
|
||||
# should only happen once, when the app starts.
|
||||
if (is.null(shinysession$restoreContext)) {
|
||||
bookmarkStore <- getShinyOption("bookmarkStore", default = "disable")
|
||||
if (bookmarkStore == "disable") {
|
||||
# If bookmarking is disabled, use empty context
|
||||
shinysession$restoreContext <- RestoreContext$new()
|
||||
} else {
|
||||
# If there's bookmarked state, save it on the session object
|
||||
shinysession$restoreContext <- RestoreContext$new(msg$data$.clientdata_url_search)
|
||||
shinysession$createBookmarkObservers()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
msg$data <- applyInputHandlers(msg$data)
|
||||
|
||||
switch(
|
||||
msg$method,
|
||||
init = {
|
||||
|
||||
serverFunc <- withReactiveDomain(NULL, serverFuncSource())
|
||||
if (!identicalFunctionBodies(serverFunc, appvars$server)) {
|
||||
appvars$server <- serverFunc
|
||||
if (!is.null(appvars$server))
|
||||
{
|
||||
# Tag this function as the Shiny server function. A debugger may use this
|
||||
# tag to give this function special treatment.
|
||||
# It's very important that it's appvars$server itself and NOT a copy that
|
||||
# is invoked, otherwise new breakpoints won't be picked up.
|
||||
attr(appvars$server, "shinyServerFunction") <- TRUE
|
||||
registerDebugHook("server", appvars, "Server Function")
|
||||
}
|
||||
}
|
||||
|
||||
# Check for switching into/out of showcase mode
|
||||
if (.globals$showcaseOverride &&
|
||||
exists(".clientdata_url_search", where = msg$data)) {
|
||||
mode <- showcaseModeOfQuerystring(msg$data$.clientdata_url_search)
|
||||
if (!is.null(mode))
|
||||
shinysession$setShowcase(mode)
|
||||
}
|
||||
|
||||
# In shinysession$createBookmarkObservers() above, observers may be
|
||||
# created, which puts the shiny session in busyCount > 0 state. That
|
||||
# prevents the manageInputs here from taking immediate effect, by
|
||||
# default. The manageInputs here needs to take effect though, because
|
||||
# otherwise the bookmark observers won't find the clientData they are
|
||||
# looking for. So use `now = TRUE` to force the changes to be
|
||||
# immediate.
|
||||
#
|
||||
# FIXME: break createBookmarkObservers into two separate steps, one
|
||||
# before and one after manageInputs, and put the observer creation
|
||||
# in the latter. Then add an assertion that busyCount == 0L when
|
||||
# this manageInputs is called.
|
||||
shinysession$manageInputs(msg$data, now = TRUE)
|
||||
|
||||
# The client tells us what singletons were rendered into
|
||||
# the initial page
|
||||
if (!is.null(msg$data$.clientdata_singletons)) {
|
||||
shinysession$singletons <- strsplit(
|
||||
msg$data$.clientdata_singletons, ',')[[1]]
|
||||
}
|
||||
|
||||
local({
|
||||
args <- argsForServerFunc(serverFunc, shinysession)
|
||||
|
||||
withReactiveDomain(shinysession, {
|
||||
otel_span_session_start(domain = shinysession, {
|
||||
|
||||
do.call(
|
||||
# No corresponding ..stacktraceoff; the server func is pure
|
||||
# user code
|
||||
wrapFunctionLabel(appvars$server, "server",
|
||||
..stacktraceon = TRUE
|
||||
),
|
||||
args
|
||||
)
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
},
|
||||
update = {
|
||||
shinysession$manageInputs(msg$data)
|
||||
},
|
||||
shinysession$dispatch(msg)
|
||||
)
|
||||
# The HTTP_GUID, if it exists, is for Shiny Server reporting purposes
|
||||
shinysession$startTiming(ws$request$HTTP_GUID)
|
||||
shinysession$requestFlush()
|
||||
|
||||
# Make httpuv return control to Shiny quickly, instead of waiting
|
||||
# for the usual timeout
|
||||
httpuv::interrupt()
|
||||
})
|
||||
}
|
||||
ws$onMessage(function(binary, msg) {
|
||||
# If unhandled errors occur, make sure they get properly logged
|
||||
withLogErrors(messageHandler(binary, msg))
|
||||
})
|
||||
|
||||
ws$onClose(function() {
|
||||
shinysession$wsClosed()
|
||||
appsByToken$remove(shinysession$token)
|
||||
appsNeedingFlush$remove(shinysession$token)
|
||||
})
|
||||
|
||||
return(TRUE)
|
||||
}
|
||||
)
|
||||
return(appHandlers)
|
||||
}
|
||||
|
||||
# Determine what arguments should be passed to this serverFunc. All server funcs
|
||||
# must take input and output, but clientData (obsolete) and session are
|
||||
# optional.
|
||||
argsForServerFunc <- function(serverFunc, session) {
|
||||
args <- list(input = session$input, output = .createOutputWriter(session))
|
||||
|
||||
paramNames <- names(formals(serverFunc))
|
||||
|
||||
# The clientData and session arguments are optional; check if
|
||||
# each exists
|
||||
|
||||
if ("clientData" %in% paramNames)
|
||||
args$clientData <- session$clientData
|
||||
|
||||
if ("session" %in% paramNames)
|
||||
args$session <- session
|
||||
|
||||
args
|
||||
}
|
||||
|
||||
getEffectiveBody <- function(func) {
|
||||
if (is.null(func))
|
||||
NULL
|
||||
else if (isS4(func) && inherits(func, "functionWithTrace"))
|
||||
body(func@original)
|
||||
else
|
||||
body(func)
|
||||
}
|
||||
|
||||
identicalFunctionBodies <- function(a, b) {
|
||||
identical(getEffectiveBody(a), getEffectiveBody(b))
|
||||
}
|
||||
|
||||
handlerManager <- HandlerManager$new()
|
||||
|
||||
addSubApp <- function(appObj, autoRemove = TRUE) {
|
||||
path <- createUniqueId(16, "/app")
|
||||
appHandlers <- createAppHandlers(appObj$httpHandler, appObj$serverFuncSource)
|
||||
|
||||
# remove the leading / from the path so a relative path is returned
|
||||
# (needed for the case where the root URL for the Shiny app isn't /, such
|
||||
# as portmapped URLs)
|
||||
finalPath <- paste(
|
||||
substr(path, 2, nchar(path)),
|
||||
"/?w=", workerId(),
|
||||
"&__subapp__=1",
|
||||
sep="")
|
||||
handlerManager$addHandler(routeHandler(path, appHandlers$http), finalPath)
|
||||
handlerManager$addWSHandler(routeWSHandler(path, appHandlers$ws), finalPath)
|
||||
|
||||
if (autoRemove) {
|
||||
# If a session is currently active, remove this subapp automatically when
|
||||
# the current session ends
|
||||
onReactiveDomainEnded(getDefaultReactiveDomain(), function() {
|
||||
removeSubApp(finalPath)
|
||||
})
|
||||
}
|
||||
|
||||
return(finalPath)
|
||||
}
|
||||
|
||||
removeSubApp <- function(path) {
|
||||
handlerManager$removeHandler(path)
|
||||
handlerManager$removeWSHandler(path)
|
||||
}
|
||||
|
||||
startApp <- function(appObj, port, host, quiet) {
|
||||
appHandlers <- createAppHandlers(appObj$httpHandler, appObj$serverFuncSource)
|
||||
handlerManager$addHandler(appHandlers$http, "/", tail = TRUE)
|
||||
handlerManager$addWSHandler(appHandlers$ws, "/", tail = TRUE)
|
||||
|
||||
httpuvApp <- handlerManager$createHttpuvApp()
|
||||
httpuvApp$staticPaths <- c(
|
||||
appObj$staticPaths,
|
||||
list(
|
||||
# Always handle /session URLs dynamically, even if / is a static path.
|
||||
"session" = excludeStaticPath(),
|
||||
"shared" = system_file(package = "shiny", "www", "shared")
|
||||
),
|
||||
.globals$resourcePaths
|
||||
)
|
||||
|
||||
# throw an informative warning if a subdirectory of the
|
||||
# app's www dir conflicts with another resource prefix
|
||||
wwwDir <- httpuvApp$staticPaths[["/"]]$path
|
||||
if (length(wwwDir)) {
|
||||
# although httpuv allows for resource prefixes like 'foo/bar',
|
||||
# we won't worry about conflicts in sub-sub directories since
|
||||
# addResourcePath() currently doesn't allow it
|
||||
wwwSubDirs <- list.dirs(wwwDir, recursive = FALSE, full.names = FALSE)
|
||||
resourceConflicts <- intersect(wwwSubDirs, names(httpuvApp$staticPaths))
|
||||
if (length(resourceConflicts)) {
|
||||
warning(
|
||||
"Found subdirectories of your app's www/ directory that ",
|
||||
"conflict with other resource URL prefixes. ",
|
||||
"Consider renaming these directories: '",
|
||||
paste0("www/", resourceConflicts, collapse = "', '"), "'",
|
||||
call. = FALSE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
# check for conflicts in each pairwise combinations of resource mappings
|
||||
checkResourceConflict <- function(paths) {
|
||||
if (length(paths) < 2) return(NULL)
|
||||
# ensure paths is a named character vector: c(resource_path = local_path)
|
||||
paths <- vapply(paths, function(x) if (inherits(x, "staticPath")) x$path else x, character(1))
|
||||
# get all possible pairwise combinations of paths
|
||||
pair_indices <- utils::combn(length(paths), 2, simplify = FALSE)
|
||||
lapply(pair_indices, function(x) {
|
||||
p1 <- paths[x[1]]
|
||||
p2 <- paths[x[2]]
|
||||
if (identical(names(p1), names(p2)) && (p1 != p2)) {
|
||||
warning(
|
||||
"Found multiple local file paths pointing the same resource prefix: ", names(p1), ". ",
|
||||
"If you run into resource-related issues (e.g. 404 requests), consider ",
|
||||
"using `addResourcePath()` and/or `removeResourcePath()` to manage resource mappings.",
|
||||
call. = FALSE
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
checkResourceConflict(httpuvApp$staticPaths)
|
||||
|
||||
httpuvApp$staticPathOptions <- httpuv::staticPathOptions(
|
||||
html_charset = "utf-8",
|
||||
headers = list("X-UA-Compatible" = "IE=edge,chrome=1"),
|
||||
validation =
|
||||
if (!is.null(getOption("shiny.sharedSecret"))) {
|
||||
sprintf('"Shiny-Shared-Secret" == "%s"', getOption("shiny.sharedSecret"))
|
||||
} else {
|
||||
character(0)
|
||||
}
|
||||
)
|
||||
|
||||
if (is.numeric(port) || is.integer(port)) {
|
||||
if (!quiet) {
|
||||
hostString <- host
|
||||
if (httpuv::ipFamily(host) == 6L)
|
||||
hostString <- paste0("[", hostString, "]")
|
||||
message('\n', 'Listening on http://', hostString, ':', port)
|
||||
}
|
||||
return(startServer(host, port, httpuvApp))
|
||||
} else if (is.character(port)) {
|
||||
if (!quiet) {
|
||||
message('\n', 'Listening on domain socket ', port)
|
||||
}
|
||||
mask <- attr(port, 'mask')
|
||||
if (is.null(mask)) {
|
||||
stop("`port` is not a valid domain socket (missing `mask` attribute). ",
|
||||
"Note that if you're using the default `host` + `port` ",
|
||||
"configuration (and not domain sockets), then `port` must ",
|
||||
"be numeric, not a string.")
|
||||
}
|
||||
return(startPipeServer(port, mask, httpuvApp))
|
||||
}
|
||||
}
|
||||
|
||||
# Run an application that was created by \code{\link{startApp}}. This
|
||||
# function should normally be called in a \code{while(TRUE)} loop.
|
||||
serviceApp <- function() {
|
||||
timerCallbacks$executeElapsed()
|
||||
|
||||
flushReact()
|
||||
flushPendingSessions()
|
||||
|
||||
# If this R session is interactive, then call service() with a short timeout
|
||||
# to keep the session responsive to user input
|
||||
maxTimeout <- ifelse(interactive(), 100, 1000)
|
||||
|
||||
timeout <- max(1, min(maxTimeout, timerCallbacks$timeToNextEvent(), later::next_op_secs()))
|
||||
service(timeout)
|
||||
|
||||
flushReact()
|
||||
flushPendingSessions()
|
||||
}
|
||||
|
||||
.shinyServerMinVersion <- '0.3.4'
|
||||
|
||||
#' Check whether a Shiny application is running
|
||||
#'
|
||||
#' This function tests whether a Shiny application is currently running.
|
||||
#'
|
||||
#' @return `TRUE` if a Shiny application is currently running. Otherwise,
|
||||
#' `FALSE`.
|
||||
#' @export
|
||||
isRunning <- function() {
|
||||
!is.null(getCurrentAppState())
|
||||
}
|
||||
|
||||
|
||||
# Returns TRUE if we're running in Shiny Server or other hosting environment,
|
||||
# otherwise returns FALSE.
|
||||
inShinyServer <- function() {
|
||||
nzchar(Sys.getenv('SHINY_PORT'))
|
||||
}
|
||||
|
||||
# This check was moved out of the main function body because of an issue with
|
||||
# the RStudio debugger. (#1474)
|
||||
isEmptyMessage <- function(msg) {
|
||||
identical(as.raw(c(0x03, 0xe9)), msg)
|
||||
}
|
||||
313
R/shiny-options.R
Normal file
313
R/shiny-options.R
Normal file
@@ -0,0 +1,313 @@
|
||||
.globals$options <- list()
|
||||
|
||||
#' @param name Name of an option to get.
|
||||
#' @param default Value to be returned if the option is not currently set.
|
||||
#' @rdname shinyOptions
|
||||
#' @export
|
||||
getShinyOption <- function(name, default = NULL) {
|
||||
# Make sure to use named (not numeric) indexing
|
||||
name <- as.character(name)
|
||||
|
||||
# Check if there's a current session
|
||||
session <- getDefaultReactiveDomain()
|
||||
if (!is.null(session)) {
|
||||
if (name %in% names(session$options)) {
|
||||
return(session$options[[name]])
|
||||
} else {
|
||||
return(default)
|
||||
}
|
||||
}
|
||||
|
||||
# Check if there's a current app
|
||||
app_state <- getCurrentAppState()
|
||||
if (!is.null(app_state)) {
|
||||
if (name %in% names(app_state$options)) {
|
||||
return(app_state$options[[name]])
|
||||
} else {
|
||||
return(default)
|
||||
}
|
||||
}
|
||||
|
||||
# If we got here, look in global options
|
||||
if (name %in% names(.globals$options)) {
|
||||
return(.globals$options[[name]])
|
||||
} else {
|
||||
return(default)
|
||||
}
|
||||
}
|
||||
|
||||
#' Get or set Shiny options
|
||||
#'
|
||||
#' @description
|
||||
#'
|
||||
#' There are two mechanisms for working with options for Shiny. One is the
|
||||
#' [options()] function, which is part of base R, and the other is the
|
||||
#' `shinyOptions()` function, which is in the Shiny package. The reason for
|
||||
#' these two mechanisms is has to do with legacy code and scoping.
|
||||
#'
|
||||
#' The [options()] function sets options globally, for the duration of the R
|
||||
#' process. The [getOption()] function retrieves the value of an option. All
|
||||
#' shiny related options of this type are prefixed with `"shiny."`.
|
||||
#'
|
||||
#' The `shinyOptions()` function sets the value of a shiny option, but unlike
|
||||
#' `options()`, it is not always global in scope; the options may be scoped
|
||||
#' globally, to an application, or to a user session in an application,
|
||||
#' depending on the context. The `getShinyOption()` function retrieves a value
|
||||
#' of a shiny option. Currently, the options set via `shinyOptions` are for
|
||||
#' internal use only.
|
||||
#'
|
||||
#' @section Options with `options()`:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{shiny.autoreload (defaults to `FALSE`)}{If `TRUE` when a Shiny app is launched, the
|
||||
#' app directory will be continually monitored for changes to files that
|
||||
#' have the extensions: r, htm, html, js, css, png, jpg, jpeg, gif. If any
|
||||
#' changes are detected, all connected Shiny sessions are reloaded. This
|
||||
#' allows for fast feedback loops when tweaking Shiny UI.
|
||||
#'
|
||||
#' Monitoring for changes is no longer expensive, thanks to the \pkg{watcher}
|
||||
#' package, but this feature is still intended only for development.
|
||||
#'
|
||||
#' You can customize the file patterns Shiny will monitor by setting the
|
||||
#' shiny.autoreload.pattern option. For example, to monitor only `ui.R`:
|
||||
#' `options(shiny.autoreload.pattern = glob2rx("ui.R"))`.
|
||||
#'
|
||||
#' As mentioned above, Shiny no longer polls watched files for changes.
|
||||
#' Instead, using \pkg{watcher}, Shiny is notified of file changes as they
|
||||
#' occur. These changes are batched together within a customizable latency
|
||||
#' period. You can adjust this period by setting
|
||||
#' `options(shiny.autoreload.interval = 2000)` (in milliseconds). This value
|
||||
#' converted to seconds and passed to the `latency` argument of
|
||||
#' [watcher::watcher()]. The default latency is 250ms.}
|
||||
#' \item{shiny.deprecation.messages (defaults to `TRUE`)}{This controls whether messages for
|
||||
#' deprecated functions in Shiny will be printed. See
|
||||
#' [shinyDeprecated()] for more information.}
|
||||
#' \item{shiny.error (defaults to `NULL`)}{This can be a function which is called when an error
|
||||
#' occurs. For example, `options(shiny.error=recover)` will result a
|
||||
#' the debugger prompt when an error occurs.}
|
||||
#' \item{shiny.fullstacktrace (defaults to `FALSE`)}{Controls whether "pretty" (`FALSE`) or full
|
||||
#' stack traces (`TRUE`) are dumped to the console when errors occur during Shiny app execution.
|
||||
#' Pretty stack traces attempt to only show user-supplied code, but this pruning can't always
|
||||
#' be done 100% correctly.}
|
||||
#' \item{shiny.host (defaults to `"127.0.0.1"`)}{The IP address that Shiny should listen on. See
|
||||
#' [runApp()] for more information.}
|
||||
#' \item{shiny.jquery.version (defaults to `3`)}{The major version of jQuery to use.
|
||||
#' Currently only values of `3` or `1` are supported. If `1`, then jQuery 1.12.4 is used. If `3`,
|
||||
#' then jQuery `r version_jquery` is used.}
|
||||
#' \item{shiny.json.digits (defaults to `I(16)`)}{Max number of digits to use when converting
|
||||
#' numbers to JSON format to send to the client web browser. Use [I()] to specify significant digits.
|
||||
#' Use `NA` for max precision.}
|
||||
#' \item{shiny.launch.browser (defaults to `interactive()`)}{A boolean which controls the default behavior
|
||||
#' when an app is run. See [runApp()] for more information.}
|
||||
#' \item{shiny.mathjax.url (defaults to `"https://mathjax.rstudio.com/latest/MathJax.js"`)}{
|
||||
#' The URL that should be used to load MathJax, via [withMathJax()].}
|
||||
#' \item{shiny.mathjax.config (defaults to `"config=TeX-AMS-MML_HTMLorMML"`)}{The querystring
|
||||
#' used to load MathJax, via [withMathJax()].}
|
||||
#' \item{shiny.maxRequestSize (defaults to 5MB)}{This is a number which specifies the maximum
|
||||
#' web request size, which serves as a size limit for file uploads.}
|
||||
#' \item{shiny.minified (defaults to `TRUE`)}{By default
|
||||
#' Whether or not to include Shiny's JavaScript as a minified (`shiny.min.js`)
|
||||
#' or un-minified (`shiny.js`) file. The un-minified version is larger,
|
||||
#' but can be helpful for development and debugging.}
|
||||
#' \item{shiny.port (defaults to a random open port)}{A port number that Shiny will listen on. See
|
||||
#' [runApp()] for more information.}
|
||||
#' \item{shiny.reactlog (defaults to `FALSE`)}{If `TRUE`, enable logging of reactive events,
|
||||
#' which can be viewed later with the [reactlogShow()] function.
|
||||
#' This incurs a substantial performance penalty and should not be used in
|
||||
#' production.}
|
||||
#' \item{shiny.sanitize.errors (defaults to `FALSE`)}{If `TRUE`, then normal errors (i.e.
|
||||
#' errors not wrapped in `safeError`) won't show up in the app; a simple
|
||||
#' generic error message is printed instead (the error and stack trace printed
|
||||
#' to the console remain unchanged). If you want to sanitize errors in general, but you DO want a
|
||||
#' particular error `e` to get displayed to the user, then set this option
|
||||
#' to `TRUE` and use `stop(safeError(e))` for errors you want the
|
||||
#' user to see.}
|
||||
#' \item{shiny.stacktraceoffset (defaults to `TRUE`)}{If `TRUE`, then Shiny's printed stack
|
||||
#' traces will display srcrefs one line above their usual location. This is
|
||||
#' an arguably more intuitive arrangement for casual R users, as the name
|
||||
#' of a function appears next to the srcref where it is defined, rather than
|
||||
#' where it is currently being called from.}
|
||||
#' \item{shiny.suppressMissingContextError (defaults to `FALSE`)}{Normally, invoking a reactive
|
||||
#' outside of a reactive context (or [isolate()]) results in
|
||||
#' an error. If this is `TRUE`, don't error in these cases. This
|
||||
#' should only be used for debugging or demonstrations of reactivity at the
|
||||
#' console.}
|
||||
#' \item{shiny.testmode (defaults to `FALSE`)}{If `TRUE`, then various features for testing Shiny
|
||||
#' applications are enabled.}
|
||||
#' \item{shiny.snapshotsortc (defaults to `FALSE`)}{If `TRUE`, test snapshot keys
|
||||
#' for \pkg{shinytest} will be sorted consistently using the C locale. Snapshots
|
||||
#' retrieved by \pkg{shinytest2} will always sort using the C locale.}
|
||||
#' \item{shiny.trace (defaults to `FALSE`)}{Print messages sent between the R server and the web
|
||||
#' browser client to the R console. This is useful for debugging. Possible
|
||||
#' values are `"send"` (only print messages sent to the client),
|
||||
#' `"recv"` (only print messages received by the server), `TRUE`
|
||||
#' (print all messages), or `FALSE` (default; don't print any of these
|
||||
#' messages).}
|
||||
#' \item{shiny.autoload.r (defaults to `TRUE`)}{If `TRUE`, then the R/
|
||||
#' of a shiny app will automatically be sourced.}
|
||||
#' \item{shiny.useragg (defaults to `TRUE`)}{Set to `FALSE` to prevent PNG rendering via the
|
||||
#' ragg package. See [plotPNG()] for more information.}
|
||||
#' \item{shiny.usecairo (defaults to `TRUE`)}{Set to `FALSE` to prevent PNG rendering via the
|
||||
#' Cairo package. See [plotPNG()] for more information.}
|
||||
#' \item{shiny.devmode (defaults to `NULL`)}{Option to enable Shiny Developer Mode. When set,
|
||||
#' different default `getOption(key)` values will be returned. See [devmode()] for more details.}
|
||||
### Not documenting as 'shiny.devmode.verbose' is for niche use only
|
||||
# ' \item{shiny.devmode.verbose (defaults to `TRUE`)}{If `TRUE`, will display messages printed
|
||||
# ' about which options are being set. See [devmode()] for more details. }
|
||||
### (end not documenting 'shiny.devmode.verbose')
|
||||
### start shiny.client_devmode is primarily for niche, internal shinylive usage
|
||||
# ' \item{shiny.client_devmode (defaults to `FALSE`)}{If `TRUE`, enables client-
|
||||
# ' side devmode features. Currently the primary feature is the client-side
|
||||
# ' error console.}
|
||||
### end shiny.client_devmode
|
||||
#' \item{shiny.otel.collect (defaults to `Sys.getenv("SHINY_OTEL_COLLECT",
|
||||
#' "all")`)}{Determines how Shiny will interact with OpenTelemetry.
|
||||
#'
|
||||
#' Supported values:
|
||||
#' * `"none"` - No Shiny OpenTelemetry tracing.
|
||||
#' * `"session"` - Adds session start/end spans.
|
||||
#' * `"reactive_update"` - Spans for any synchronous/asynchronous reactive
|
||||
#' update. (Includes `"session"` features).
|
||||
#' * `"reactivity"` - Spans for all reactive expressions and logs for setting
|
||||
#' reactive vals and values. (Includes `"reactive_update"` features). This
|
||||
#' option must be set when creating any reactive objects that should record
|
||||
#' OpenTelemetry spans / logs. See [`withOtelCollect()`] and
|
||||
#' [`localOtelCollect()`] for ways to set this option locally when creating
|
||||
#' your reactive expressions.
|
||||
#' * `"all"` - All Shiny OpenTelemetry tracing. Currently equivalent to
|
||||
#' `"reactivity"`.
|
||||
#'
|
||||
#' This option is useful for debugging and profiling while in production. This
|
||||
#' option will only be useful if the `otelsdk` package is installed and
|
||||
#' `otel::is_tracing_enabled()` returns `TRUE`. Please have any OpenTelemetry
|
||||
#' environment variables set before loading any relevant R packages.
|
||||
#'
|
||||
#' To set this option locally within a specific part of your Shiny
|
||||
#' application, see [`withOtelCollect()`] and [`localOtelCollect()`].}
|
||||
#' \item{shiny.otel.sanitize.errors (defaults to `TRUE`)}{If `TRUE`, fatal and unhandled errors will be sanitized before being sent to the OpenTelemetry backend. The default value of `TRUE` is set to avoid potentially sending sensitive information to the OpenTelemetry backend. If you want the full error message and stack trace to be sent to the OpenTelemetry backend, set this option to `FALSE` or use `safeError(e)`.}
|
||||
#' }
|
||||
#'
|
||||
#'
|
||||
#' @section Scoping for `shinyOptions()`:
|
||||
#'
|
||||
#' There are three levels of scoping for `shinyOptions()`: global,
|
||||
#' application, and session.
|
||||
#'
|
||||
#' The global option set is available by default. Any calls to
|
||||
#' `shinyOptions()` and `getShinyOption()` outside of an app will access the
|
||||
#' global option set.
|
||||
#'
|
||||
#' When a Shiny application is run with [runApp()], the global option set is
|
||||
#' duplicated and the new option set is available at the application level. If
|
||||
#' options are set from `global.R`, `app.R`, `ui.R`, or `server.R` (but
|
||||
#' outside of the server function), then the application-level options will be
|
||||
#' modified.
|
||||
#'
|
||||
#' Each time a user session is started, the application-level option set is
|
||||
#' duplicated, for that session. If the options are set from inside the server
|
||||
#' function, then they will be scoped to the session.
|
||||
#'
|
||||
#' @section Options with `shinyOptions()`:
|
||||
#'
|
||||
#' There are a number of global options that affect Shiny's behavior. These
|
||||
#' can be set globally with `options()` or locally (for a single app) with
|
||||
#' `shinyOptions()`.
|
||||
#'
|
||||
#' \describe{ \item{cache}{A caching object that will be used by
|
||||
#' [renderCachedPlot()]. If not specified, a [cachem::cache_mem()] will be
|
||||
#' used.} }
|
||||
#'
|
||||
#' @param ... Options to set, with the form `name = value`.
|
||||
#' @aliases shiny-options
|
||||
#' @export
|
||||
shinyOptions <- function(...) {
|
||||
newOpts <- list2(...)
|
||||
|
||||
if (length(newOpts) > 0) {
|
||||
# If we're within a session, modify at the session level.
|
||||
session <- getDefaultReactiveDomain()
|
||||
if (!is.null(session)) {
|
||||
# Modify session-level-options
|
||||
session$options <- dropNulls(mergeVectors(session$options, newOpts))
|
||||
return(invisible(session$options))
|
||||
}
|
||||
|
||||
# If not in a session, but we have a currently running app, modify options
|
||||
# at the app level.
|
||||
app_state <- getCurrentAppState()
|
||||
if (!is.null(app_state)) {
|
||||
# Modify app-level options
|
||||
app_state$options <- dropNulls(mergeVectors(app_state$options, newOpts))
|
||||
return(invisible(app_state$options))
|
||||
}
|
||||
|
||||
# If no currently running app, modify global options and return them.
|
||||
.globals$options <- dropNulls(mergeVectors(.globals$options, newOpts))
|
||||
return(invisible(.globals$options))
|
||||
}
|
||||
|
||||
# If not setting any options, just return current option set, visibly.
|
||||
|
||||
session <- getDefaultReactiveDomain()
|
||||
if (!is.null(session)) {
|
||||
return(session$options)
|
||||
}
|
||||
|
||||
app_state <- getCurrentAppState()
|
||||
if (!is.null(app_state)) {
|
||||
return(app_state$options)
|
||||
}
|
||||
|
||||
return(.globals$options)
|
||||
}
|
||||
|
||||
|
||||
# Get specific shiny options and put them in a list, reset those shiny options,
|
||||
# and then return the options list. This should be during the creation of a
|
||||
# shiny app object. This function "consumes" the options when the shinyApp
|
||||
# object is created, so the options won't affect another app that is created
|
||||
# later.
|
||||
#
|
||||
# ==== Example ====
|
||||
# shinyOptions(bookmarkStore = 1234)
|
||||
# # This now returns 1234.
|
||||
# getShinyOption("bookmarkStore")
|
||||
#
|
||||
# # Creating the app captures the bookmarkStore option and clears it.
|
||||
# s <- shinyApp(
|
||||
# fluidPage(verbatimTextOutput("txt")),
|
||||
# function(input, output) {
|
||||
# output$txt <- renderText(getShinyOption("bookmarkStore"))
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# # This now returns NULL.
|
||||
# getShinyOption("bookmarkStore")
|
||||
#
|
||||
# When running the app, the app will display "1234"
|
||||
# runApp(s)
|
||||
#
|
||||
# # After quitting the app, this still returns NULL.
|
||||
# getShinyOption("bookmarkStore")
|
||||
# ==================
|
||||
#
|
||||
# If another app had been created after s was created, but before s was run,
|
||||
# then it would capture the value of "bookmarkStore" at the time of creation.
|
||||
captureAppOptions <- function() {
|
||||
options <- list(
|
||||
appDir = getwd(),
|
||||
bookmarkStore = getShinyOption("bookmarkStore")
|
||||
)
|
||||
|
||||
shinyOptions(appDir = NULL, bookmarkStore = NULL)
|
||||
|
||||
options
|
||||
}
|
||||
|
||||
# Do the inverse of captureAppOptions. This should be called once the app is
|
||||
# started.
|
||||
applyCapturedAppOptions <- function(options) {
|
||||
if (!is.null(options)) {
|
||||
do.call(shinyOptions, options)
|
||||
}
|
||||
}
|
||||
44
R/shiny-package.R
Normal file
44
R/shiny-package.R
Normal file
@@ -0,0 +1,44 @@
|
||||
# See also R/reexports.R
|
||||
|
||||
## usethis namespace: start
|
||||
#' @importFrom lifecycle deprecated is_present
|
||||
#' @importFrom grDevices dev.set dev.cur
|
||||
#' @importFrom fastmap fastmap
|
||||
#' @importFrom promises
|
||||
#' %...!% %...>%
|
||||
#' as.promise is.promising is.promise
|
||||
#' promise_resolve promise_reject
|
||||
#' hybrid_then
|
||||
#' with_promise_domain new_promise_domain
|
||||
#' @importFrom rlang
|
||||
#' quo enquo enquo0 as_function get_expr get_env new_function enquos
|
||||
#' eval_tidy expr pairlist2 new_quosure enexpr as_quosure is_quosure inject
|
||||
#' quo_set_env quo_set_expr quo_get_expr
|
||||
#' enquos0 zap_srcref %||% is_na
|
||||
#' is_false list2
|
||||
#' missing_arg is_missing maybe_missing
|
||||
#' quo_is_missing fn_fmls<- fn_body fn_body<-
|
||||
#' check_dots_empty check_dots_unnamed
|
||||
#' @import htmltools
|
||||
#' @import httpuv
|
||||
#' @import xtable
|
||||
#' @import R6
|
||||
#' @import mime
|
||||
## usethis namespace: end
|
||||
NULL
|
||||
|
||||
# It's necessary to Depend on methods so Rscript doesn't fail. It's necessary
|
||||
# to import(methods) in NAMESPACE so R CMD check doesn't complain. This
|
||||
# approach isn't foolproof because Rscript -e pkgname::func() doesn't actually
|
||||
# cause methods to be attached, but it's not a problem for shiny::runApp()
|
||||
# since we call require(shiny) as part of loading the app.
|
||||
#' @import methods
|
||||
NULL
|
||||
|
||||
|
||||
# For usethis::use_release_issue()
|
||||
release_bullets <- function() {
|
||||
c(
|
||||
"Update static imports: `staticimports::import()`"
|
||||
)
|
||||
}
|
||||
669
R/shinyapp.R
Normal file
669
R/shinyapp.R
Normal file
@@ -0,0 +1,669 @@
|
||||
# TODO: Subapp global.R
|
||||
|
||||
#' Create a Shiny app object
|
||||
#'
|
||||
#' These functions create Shiny app objects from either an explicit UI/server
|
||||
#' pair (`shinyApp`), or by passing the path of a directory that contains a
|
||||
#' Shiny app (`shinyAppDir`).
|
||||
#'
|
||||
#' Normally when this function is used at the R console, the Shiny app object is
|
||||
#' automatically passed to the `print()` function, which runs the app. If
|
||||
#' this is called in the middle of a function, the value will not be passed to
|
||||
#' `print()` and the app will not be run. To make the app run, pass the app
|
||||
#' object to `print()` or [runApp()].
|
||||
#'
|
||||
#' @param ui The UI definition of the app (for example, a call to
|
||||
#' `fluidPage()` with nested controls).
|
||||
#'
|
||||
#' If bookmarking is enabled (see `enableBookmarking`), this must be
|
||||
#' a single argument function that returns the UI definition.
|
||||
#' @param server A function with three parameters: `input`, `output`, and
|
||||
#' `session`. The function is called once for each session ensuring that each
|
||||
#' app is independent.
|
||||
#' @param onStart A function that will be called before the app is actually run.
|
||||
#' This is only needed for `shinyAppObj`, since in the `shinyAppDir`
|
||||
#' case, a `global.R` file can be used for this purpose.
|
||||
#' @param options Named options that should be passed to the `runApp` call
|
||||
#' (these can be any of the following: "port", "launch.browser", "host", "quiet",
|
||||
#' "display.mode" and "test.mode"). You can also specify `width` and
|
||||
#' `height` parameters which provide a hint to the embedding environment
|
||||
#' about the ideal height/width for the app.
|
||||
#' @param uiPattern A regular expression that will be applied to each `GET`
|
||||
#' request to determine whether the `ui` should be used to handle the
|
||||
#' request. Note that the entire request path must match the regular
|
||||
#' expression in order for the match to be considered successful.
|
||||
#' @param enableBookmarking Can be one of `"url"`, `"server"`, or
|
||||
#' `"disable"`. The default value, `NULL`, will respect the setting from
|
||||
#' any previous calls to [enableBookmarking()]. See [enableBookmarking()]
|
||||
#' for more information on bookmarking your app.
|
||||
#' @return An object that represents the app. Printing the object or passing it
|
||||
#' to [runApp()] will run the app.
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' numericInput("n", "n", 1),
|
||||
#' plotOutput("plot")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$plot <- renderPlot( plot(head(cars, input$n)) )
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' shinyAppDir(system.file("examples/01_hello", package="shiny"))
|
||||
#'
|
||||
#'
|
||||
#' # The object can be passed to runApp()
|
||||
#' app <- shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' numericInput("n", "n", 1),
|
||||
#' plotOutput("plot")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$plot <- renderPlot( plot(head(cars, input$n)) )
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' runApp(app)
|
||||
#' }
|
||||
#' @export
|
||||
shinyApp <- function(ui, server, onStart=NULL, options=list(),
|
||||
uiPattern="/", enableBookmarking=NULL) {
|
||||
if (!is.function(server)) {
|
||||
stop("`server` must be a function", call. = FALSE)
|
||||
}
|
||||
|
||||
# Ensure that the entire path is a match
|
||||
uiPattern <- sprintf("^%s$", uiPattern)
|
||||
|
||||
httpHandler <- uiHttpHandler(ui, uiPattern)
|
||||
|
||||
serverFuncSource <- function() {
|
||||
server
|
||||
}
|
||||
|
||||
if (!is.null(enableBookmarking)) {
|
||||
bookmarkStore <- match.arg(enableBookmarking, c("url", "server", "disable"))
|
||||
enableBookmarking(bookmarkStore)
|
||||
}
|
||||
|
||||
# Store the appDir and bookmarking-related options, so that we can read them
|
||||
# from within the app.
|
||||
appOptions <- captureAppOptions()
|
||||
|
||||
structure(
|
||||
list(
|
||||
httpHandler = httpHandler,
|
||||
serverFuncSource = serverFuncSource,
|
||||
onStart = onStart,
|
||||
options = options,
|
||||
appOptions = appOptions
|
||||
),
|
||||
class = "shiny.appobj"
|
||||
)
|
||||
}
|
||||
|
||||
#' @rdname shinyApp
|
||||
#' @param appDir Path to directory that contains a Shiny app (i.e. a server.R
|
||||
#' file and either ui.R or www/index.html)
|
||||
#' @export
|
||||
shinyAppDir <- function(appDir, options=list()) {
|
||||
if (!utils::file_test('-d', appDir)) {
|
||||
rlang::abort(
|
||||
paste0("No Shiny application exists at the path \"", appDir, "\""),
|
||||
class = "invalidShinyAppDir"
|
||||
)
|
||||
}
|
||||
|
||||
# In case it's a relative path, convert to absolute (so we're not adversely
|
||||
# affected by future changes to the path)
|
||||
appDir <- normalizePath(appDir, mustWork = TRUE)
|
||||
|
||||
if (file.exists.ci(appDir, "server.R")) {
|
||||
shinyAppDir_serverR(appDir, options = options)
|
||||
} else if (file.exists.ci(appDir, "app.R")) {
|
||||
shinyAppDir_appR("app.R", appDir, options = options)
|
||||
} else {
|
||||
rlang::abort(
|
||||
"App dir must contain either app.R or server.R.",
|
||||
class = "invalidShinyAppDir"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#' @rdname shinyApp
|
||||
#' @param appFile Path to a .R file containing a Shiny application
|
||||
#' @export
|
||||
shinyAppFile <- function(appFile, options=list()) {
|
||||
appFile <- normalizePath(appFile, mustWork = TRUE)
|
||||
appDir <- dirname(appFile)
|
||||
|
||||
shinyAppDir_appR(basename(appFile), appDir, options = options)
|
||||
}
|
||||
|
||||
# This reads in an app dir in the case that there's a server.R (and ui.R/www)
|
||||
# present, and returns a shiny.appobj.
|
||||
# appDir must be a normalized (absolute) path, not a relative one
|
||||
shinyAppDir_serverR <- function(appDir, options=list()) {
|
||||
# Most of the complexity here comes from needing to hot-reload if the .R files
|
||||
# change on disk, or are created, or are removed.
|
||||
|
||||
# In an upcoming version of shiny, this option will go away.
|
||||
if (getOption("shiny.autoload.r", TRUE)) {
|
||||
# Create a child env which contains all the helpers and will be the shared parent
|
||||
# of the ui.R and server.R load.
|
||||
sharedEnv <- new.env(parent = globalenv())
|
||||
} else {
|
||||
# old behavior
|
||||
sharedEnv <- globalenv()
|
||||
}
|
||||
|
||||
# To enable hot-reloading of support files, this function is called
|
||||
# whenever the UI or Server func source is updated. To avoid loading
|
||||
# support files 2x, we follow the last cache update trigger timestamp.
|
||||
autoload_r_support_if_needed <- local({
|
||||
autoload_last_loaded <- -1
|
||||
function() {
|
||||
if (!isTRUE(getOption("shiny.autoload.r", TRUE))) return()
|
||||
|
||||
last_cache_trigger <- cachedAutoReloadLastChanged$get()
|
||||
if (identical(autoload_last_loaded, last_cache_trigger)) return()
|
||||
|
||||
loadSupport(appDir, renv = sharedEnv, globalrenv = globalenv())
|
||||
|
||||
autoload_last_loaded <<- last_cache_trigger
|
||||
}
|
||||
})
|
||||
|
||||
# uiHandlerSource is a function that returns an HTTP handler for serving up
|
||||
# ui.R as a webpage. The "cachedFuncWithFile" call makes sure that the closure
|
||||
# we're creating here only gets executed when ui.R's contents change.
|
||||
uiHandlerSource <- cachedFuncWithFile(appDir, "ui.R", case.sensitive = FALSE,
|
||||
function(uiR) {
|
||||
autoload_r_support_if_needed()
|
||||
if (file.exists(uiR)) {
|
||||
# If ui.R contains a call to shinyUI (which sets .globals$ui), use that.
|
||||
# If not, then take the last expression that's returned from ui.R.
|
||||
.globals$ui <- NULL
|
||||
on.exit(.globals$ui <- NULL, add = FALSE)
|
||||
ui <- sourceUTF8(uiR, envir = new.env(parent = sharedEnv))
|
||||
if (!is.null(.globals$ui)) {
|
||||
ui <- .globals$ui[[1]]
|
||||
}
|
||||
return(uiHttpHandler(ui))
|
||||
} else {
|
||||
return(function(req) NULL)
|
||||
}
|
||||
}
|
||||
)
|
||||
uiHandler <- function(req) {
|
||||
uiHandlerSource()(req)
|
||||
}
|
||||
|
||||
wwwDir <- file.path.ci(appDir, "www")
|
||||
if (dirExists(wwwDir)) {
|
||||
staticPaths <- list("/" = staticPath(wwwDir, indexhtml = FALSE, fallthrough = TRUE))
|
||||
} else {
|
||||
staticPaths <- list()
|
||||
}
|
||||
|
||||
fallbackWWWDir <- system_file("www-dir", package = "shiny")
|
||||
|
||||
serverSource <- cachedFuncWithFile(appDir, "server.R", case.sensitive = FALSE,
|
||||
function(serverR) {
|
||||
autoload_r_support_if_needed()
|
||||
# If server.R contains a call to shinyServer (which sets .globals$server),
|
||||
# use that. If not, then take the last expression that's returned from
|
||||
# server.R.
|
||||
.globals$server <- NULL
|
||||
on.exit(.globals$server <- NULL, add = TRUE)
|
||||
result <- sourceUTF8(serverR, envir = new.env(parent = sharedEnv))
|
||||
if (!is.null(.globals$server)) {
|
||||
result <- .globals$server[[1]]
|
||||
}
|
||||
return(result)
|
||||
}
|
||||
)
|
||||
|
||||
# This function stands in for the server function, and reloads the
|
||||
# real server function as necessary whenever server.R changes
|
||||
serverFuncSource <- function() {
|
||||
serverFunction <- serverSource()
|
||||
if (is.null(serverFunction)) {
|
||||
return(function(input, output) NULL)
|
||||
} else if (is.function(serverFunction)) {
|
||||
# This is what we normally expect; run the server function
|
||||
return(serverFunction)
|
||||
} else {
|
||||
stop("server.R returned an object of unexpected type: ",
|
||||
typeof(serverFunction))
|
||||
}
|
||||
}
|
||||
|
||||
shinyOptions(appDir = appDir)
|
||||
|
||||
oldwd <- NULL
|
||||
monitorHandle <- NULL
|
||||
onStart <- function() {
|
||||
oldwd <<- getwd()
|
||||
setwd(appDir)
|
||||
if (getOption("shiny.autoload.r", TRUE)) {
|
||||
autoload_r_support_if_needed()
|
||||
} else {
|
||||
if (file.exists(file.path.ci(appDir, "global.R")))
|
||||
sourceUTF8(file.path.ci(appDir, "global.R"))
|
||||
}
|
||||
monitorHandle <<- initAutoReloadMonitor(appDir)
|
||||
}
|
||||
onStop <- function() {
|
||||
setwd(oldwd)
|
||||
# It is possible that while calling appObj()$onStart() or loadingSupport, an error occured
|
||||
# This will cause `onStop` to be called.
|
||||
# The `oldwd` will exist, but `monitorHandle` is not a function yet.
|
||||
if (is.function(monitorHandle)) {
|
||||
monitorHandle()
|
||||
monitorHandle <<- NULL
|
||||
}
|
||||
}
|
||||
|
||||
structure(
|
||||
list(
|
||||
staticPaths = staticPaths,
|
||||
# Even though the wwwDir is handled as a static path, we need to include
|
||||
# it here to be handled by R as well. This is because the special case
|
||||
# of index.html: it is specifically not handled as a staticPath for
|
||||
# reasons explained above, but if someone does want to serve up an
|
||||
# index.html, we need to handle it, and we do it by using the
|
||||
# staticHandler in the R code path. (#2380)
|
||||
httpHandler = joinHandlers(c(uiHandler, wwwDir, fallbackWWWDir)),
|
||||
serverFuncSource = serverFuncSource,
|
||||
onStart = onStart,
|
||||
onStop = onStop,
|
||||
options = options
|
||||
),
|
||||
class = "shiny.appobj"
|
||||
)
|
||||
}
|
||||
|
||||
# Start a reactive observer that continually monitors dir for changes to files
|
||||
# that have the extensions: r, htm, html, js, css, png, jpg, jpeg, gif. Case is
|
||||
# ignored when checking extensions. If any changes are detected, all connected
|
||||
# Shiny sessions are reloaded.
|
||||
#
|
||||
# Use options(shiny.autoreload = TRUE) to enable this behavior. Since monitoring
|
||||
# for changes is expensive (we are polling for mtimes here, nothing fancy) this
|
||||
# feature is intended only for development.
|
||||
#
|
||||
# You can customize the file patterns Shiny will monitor by setting the
|
||||
# shiny.autoreload.pattern option. For example, to monitor only ui.R:
|
||||
# options(shiny.autoreload.pattern = glob2rx("ui.R"))
|
||||
#
|
||||
# The return value is a function that halts monitoring when called.
|
||||
initAutoReloadMonitor <- function(dir) {
|
||||
if (!get_devmode_option("shiny.autoreload", FALSE)) {
|
||||
return(function(){})
|
||||
}
|
||||
|
||||
filePattern <- getOption(
|
||||
"shiny.autoreload.pattern",
|
||||
".*\\.(r|html?|js|css|png|jpe?g|gif)$"
|
||||
)
|
||||
|
||||
|
||||
if (is_installed("watcher")) {
|
||||
check_for_update <- function(paths) {
|
||||
paths <- grep(
|
||||
filePattern,
|
||||
paths,
|
||||
ignore.case = TRUE,
|
||||
value = TRUE
|
||||
)
|
||||
|
||||
if (length(paths) == 0) {
|
||||
return()
|
||||
}
|
||||
|
||||
cachedAutoReloadLastChanged$set()
|
||||
autoReloadCallbacks$invoke()
|
||||
}
|
||||
|
||||
# [garrick, 2025-02-20] Shiny <= v1.10.0 used `invalidateLater()` with an
|
||||
# autoreload.interval in ms. {watcher} instead uses a latency parameter in
|
||||
# seconds, which serves a similar purpose and that I'm keeping for backcompat.
|
||||
latency <- getOption("shiny.autoreload.interval", 250) / 1000
|
||||
watcher <- watcher::watcher(dir, check_for_update, latency = latency)
|
||||
watcher$start()
|
||||
onStop(watcher$stop)
|
||||
} else {
|
||||
# Fall back to legacy observer behavior
|
||||
if (!is_false(getOption("shiny.autoreload.legacy_warning", TRUE))) {
|
||||
cli::cli_warn(
|
||||
c(
|
||||
"Using legacy autoreload file watching. Please install {.pkg watcher} for a more performant autoreload file watcher.",
|
||||
"i" = "Set {.run options(shiny.autoreload.legacy_warning = FALSE)} to suppress this warning."
|
||||
),
|
||||
.frequency = "regularly",
|
||||
.frequency_id = "shiny.autoreload.legacy_warning"
|
||||
)
|
||||
}
|
||||
|
||||
lastValue <- NULL
|
||||
observeLabel <- paste0("File Auto-Reload - '", basename(dir), "'")
|
||||
watcher <- observe(label = observeLabel, {
|
||||
files <- sort_c(
|
||||
list.files(dir, pattern = filePattern, recursive = TRUE, ignore.case = TRUE)
|
||||
)
|
||||
times <- file.info(files)$mtime
|
||||
names(times) <- files
|
||||
|
||||
if (is.null(lastValue)) {
|
||||
# First run
|
||||
lastValue <<- times
|
||||
} else if (!identical(lastValue, times)) {
|
||||
# We've changed!
|
||||
lastValue <<- times
|
||||
cachedAutoReloadLastChanged$set()
|
||||
autoReloadCallbacks$invoke()
|
||||
}
|
||||
|
||||
invalidateLater(getOption("shiny.autoreload.interval", 500))
|
||||
})
|
||||
|
||||
onStop(watcher$destroy)
|
||||
|
||||
watcher$destroy
|
||||
}
|
||||
|
||||
invisible(watcher)
|
||||
}
|
||||
|
||||
#' Load an app's supporting R files
|
||||
#'
|
||||
#' Loads all of the supporting R files of a Shiny application. Specifically,
|
||||
#' this function loads any top-level supporting `.R` files in the `R/` directory
|
||||
#' adjacent to the `app.R`/`server.R`/`ui.R` files.
|
||||
#'
|
||||
#' Since Shiny 1.5.0, this function is called by default when running an
|
||||
#' application. If it causes problems, there are two ways to opt out. You can
|
||||
#' either place a file named `_disable_autoload.R` in your R/ directory, or
|
||||
#' set `options(shiny.autoload.r=FALSE)`. If you set this option, it will
|
||||
#' affect any application that runs later in the same R session, potentially
|
||||
#' breaking it, so after running your application, you should unset option with
|
||||
#' `options(shiny.autoload.r=NULL)`
|
||||
#'
|
||||
#' @details The files are sourced in alphabetical order (as determined by
|
||||
#' [list.files]). `global.R` is evaluated before the supporting R files in the
|
||||
#' `R/` directory.
|
||||
#' @param appDir The application directory. If `appDir` is `NULL` or
|
||||
#' not supplied, the nearest enclosing directory that is a Shiny app, starting
|
||||
#' with the current directory, is used.
|
||||
#' @param renv The environment in which the files in the `R/` directory should
|
||||
#' be evaluated.
|
||||
#' @param globalrenv The environment in which `global.R` should be evaluated. If
|
||||
#' `NULL`, `global.R` will not be evaluated at all.
|
||||
#' @export
|
||||
loadSupport <- function(appDir=NULL, renv=new.env(parent=globalenv()), globalrenv=globalenv()){
|
||||
require(shiny)
|
||||
|
||||
if (is.null(appDir)) {
|
||||
appDir <- findEnclosingApp(".")
|
||||
}
|
||||
|
||||
if (!is.null(globalrenv)){
|
||||
# Evaluate global.R, if it exists.
|
||||
globalPath <- file.path.ci(appDir, "global.R")
|
||||
if (file.exists(globalPath)){
|
||||
withr::with_dir(appDir, {
|
||||
sourceUTF8(basename(globalPath), envir=globalrenv)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
helpersDir <- file.path(appDir, "R")
|
||||
|
||||
disabled <- list.files(helpersDir, pattern="^_disable_autoload\\.r$", recursive=FALSE, ignore.case=TRUE)
|
||||
if (length(disabled) > 0) {
|
||||
return(invisible(renv))
|
||||
}
|
||||
|
||||
warn_if_app_dir_is_package(appDir)
|
||||
|
||||
helpers <- list.files(helpersDir, pattern="\\.[rR]$", recursive=FALSE, full.names=TRUE)
|
||||
# Ensure files in R/ are sorted according to the 'C' locale before sourcing.
|
||||
# This convention is based on the default for packages. For details, see:
|
||||
# https://cran.r-project.org/doc/manuals/r-release/R-exts.html#The-DESCRIPTION-file
|
||||
helpers <- sort_c(helpers)
|
||||
helpers <- normalizePath(helpers)
|
||||
|
||||
withr::with_dir(appDir, {
|
||||
lapply(helpers, sourceUTF8, envir=renv)
|
||||
})
|
||||
|
||||
invisible(renv)
|
||||
}
|
||||
|
||||
warn_if_app_dir_is_package <- function(appDir) {
|
||||
has_namespace <- file.exists(file.path.ci(appDir, "NAMESPACE"))
|
||||
has_desc_pkg <- FALSE
|
||||
|
||||
if (!has_namespace) {
|
||||
descFile <- file.path.ci(appDir, "DESCRIPTION")
|
||||
|
||||
has_desc_pkg <-
|
||||
file.exists(descFile) &&
|
||||
identical(as.character(read.dcf(descFile, fields = "Type")), "Package")
|
||||
}
|
||||
|
||||
if (has_namespace || has_desc_pkg) {
|
||||
warning(
|
||||
"Loading R/ subdirectory for Shiny application, but this directory appears ",
|
||||
"to contain an R package. Sourcing files in R/ may cause unexpected behavior. ",
|
||||
"See `?loadSupport` for more details."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
# This reads in an app dir for a single-file application (e.g. app.R), and
|
||||
# returns a shiny.appobj.
|
||||
# appDir must be a normalized (absolute) path, not a relative one
|
||||
shinyAppDir_appR <- function(fileName, appDir, options=list())
|
||||
{
|
||||
fullpath <- file.path.ci(appDir, fileName)
|
||||
|
||||
# This sources app.R and caches the content. When appObj() is called but
|
||||
# app.R hasn't changed, it won't re-source the file. But if called and
|
||||
# app.R has changed, it'll re-source the file and return the result.
|
||||
appObj <- cachedFuncWithFile(appDir, fileName, case.sensitive = FALSE,
|
||||
function(appR) {
|
||||
wasDir <- setwd(appDir)
|
||||
on.exit(setwd(wasDir))
|
||||
|
||||
if (getOption("shiny.autoload.r", TRUE)) {
|
||||
# Create a child env which contains all the helpers and will be the shared parent
|
||||
# of the ui.R and server.R load.
|
||||
sharedEnv <- new.env(parent = globalenv())
|
||||
loadSupport(appDir, renv=sharedEnv, globalrenv=NULL)
|
||||
} else {
|
||||
sharedEnv <- globalenv()
|
||||
}
|
||||
result <- sourceUTF8(fullpath, envir = new.env(parent = sharedEnv))
|
||||
|
||||
if (!is.shiny.appobj(result))
|
||||
stop("app.R did not return a shiny.appobj object.")
|
||||
|
||||
applyCapturedAppOptions(result$appOptions)
|
||||
|
||||
return(result)
|
||||
}
|
||||
)
|
||||
|
||||
# A function that invokes the http handler from the appObj in app.R, but
|
||||
# since this uses appObj(), it only re-sources the file when it changes.
|
||||
dynHttpHandler <- function(...) {
|
||||
appObj()$httpHandler(...)
|
||||
}
|
||||
|
||||
dynServerFuncSource <- function(...) {
|
||||
appObj()$serverFuncSource(...)
|
||||
}
|
||||
|
||||
wwwDir <- file.path.ci(appDir, "www")
|
||||
if (dirExists(wwwDir)) {
|
||||
# wwwDir is a static path served by httpuv. It does _not_ serve up
|
||||
# index.html, for two reasons. (1) It's possible that the user's
|
||||
# www/index.html file is not actually used as the index, but as a template
|
||||
# that gets processed before being sent; and (2) the index content may be
|
||||
# modified by the hosting environment (as in SockJSAdapter.R).
|
||||
#
|
||||
# The call to staticPath normalizes the path, so that if the working dir
|
||||
# later changes, it will continue to point to the right place.
|
||||
staticPaths <- list("/" = staticPath(wwwDir, indexhtml = FALSE, fallthrough = TRUE))
|
||||
} else {
|
||||
staticPaths <- list()
|
||||
}
|
||||
|
||||
fallbackWWWDir <- system_file("www-dir", package = "shiny")
|
||||
|
||||
oldwd <- NULL
|
||||
monitorHandle <- NULL
|
||||
onStart <- function() {
|
||||
oldwd <<- getwd()
|
||||
setwd(appDir)
|
||||
if (!is.null(appObj()$onStart)) appObj()$onStart()
|
||||
monitorHandle <<- initAutoReloadMonitor(appDir)
|
||||
invisible()
|
||||
}
|
||||
onStop <- function() {
|
||||
setwd(oldwd)
|
||||
# It is possible that while calling appObj()$onStart() or loadingSupport, an error occured
|
||||
# This will cause `onStop` to be called.
|
||||
# The `oldwd` will exist, but `monitorHandle` is not a function yet.
|
||||
if (is.function(monitorHandle)) {
|
||||
monitorHandle()
|
||||
monitorHandle <<- NULL
|
||||
}
|
||||
}
|
||||
|
||||
appObjOptions <- appObj()$options
|
||||
|
||||
structure(
|
||||
list(
|
||||
# fallbackWWWDir is _not_ listed in staticPaths, because it needs to
|
||||
# come after the uiHandler. It also does not need to be fast, since it
|
||||
# should rarely be hit. The order is wwwDir (in staticPaths), then
|
||||
# uiHandler, then falbackWWWDir (which is served up by the R
|
||||
# staticHandler function).
|
||||
staticPaths = staticPaths,
|
||||
# Even though the wwwDir is handled as a static path, we need to include
|
||||
# it here to be handled by R as well. This is because the special case
|
||||
# of index.html: it is specifically not handled as a staticPath for
|
||||
# reasons explained above, but if someone does want to serve up an
|
||||
# index.html, we need to handle it, and we do it by using the
|
||||
# staticHandler in the R code path. (#2380)
|
||||
httpHandler = joinHandlers(c(dynHttpHandler, wwwDir, fallbackWWWDir)),
|
||||
serverFuncSource = dynServerFuncSource,
|
||||
onStart = onStart,
|
||||
onStop = onStop,
|
||||
options = joinOptions(appObjOptions, options)
|
||||
),
|
||||
class = "shiny.appobj"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' Shiny App object
|
||||
#'
|
||||
#' Internal methods for the `shiny.appobj` S3 class.
|
||||
#'
|
||||
#' @keywords internal
|
||||
#' @name shiny.appobj
|
||||
NULL
|
||||
|
||||
#' @rdname shiny.appobj
|
||||
#' @param x Object to convert to a Shiny app.
|
||||
#' @export
|
||||
as.shiny.appobj <- function(x) {
|
||||
UseMethod("as.shiny.appobj", x)
|
||||
}
|
||||
|
||||
#' @rdname shiny.appobj
|
||||
#' @export
|
||||
as.shiny.appobj.shiny.appobj <- function(x) {
|
||||
x
|
||||
}
|
||||
|
||||
#' @rdname shiny.appobj
|
||||
#' @export
|
||||
as.shiny.appobj.list <- function(x) {
|
||||
shinyApp(ui = x$ui, server = x$server)
|
||||
}
|
||||
|
||||
#' @rdname shiny.appobj
|
||||
#' @export
|
||||
as.shiny.appobj.character <- function(x) {
|
||||
if (identical(tolower(tools::file_ext(x)), "r"))
|
||||
shinyAppFile(x)
|
||||
else
|
||||
shinyAppDir(x)
|
||||
}
|
||||
|
||||
#' @rdname shiny.appobj
|
||||
#' @export
|
||||
is.shiny.appobj <- function(x) {
|
||||
inherits(x, "shiny.appobj")
|
||||
}
|
||||
|
||||
#' @rdname shiny.appobj
|
||||
#' @param ... Ignored.
|
||||
#' @export
|
||||
print.shiny.appobj <- function(x, ...) {
|
||||
runApp(x)
|
||||
}
|
||||
|
||||
# Joins two options objects (i.e. the `options` argument to shinyApp(),
|
||||
# shinyAppDir(), etc.). The values in `b` should take precedence over the values
|
||||
# in `a`. Given the current options available, it is safe to throw away any
|
||||
# values in `a` that are provided in `b`. But in the future, if new options are
|
||||
# introduced that need to be combined in some way instead of simply overwritten,
|
||||
# then this will be the place to do it. See the implementations of
|
||||
# print.shiny.appobj() and runApp() (for the latter, look specifically for
|
||||
# "findVal()") to determine the set of possible options.
|
||||
joinOptions <- function(a, b) {
|
||||
stopifnot(is.null(a) || is.list(a))
|
||||
stopifnot(is.null(b) || is.list(b))
|
||||
|
||||
mergeVectors(a, b)
|
||||
}
|
||||
|
||||
#' @rdname shiny.appobj
|
||||
#' @method as.tags shiny.appobj
|
||||
#' @export
|
||||
as.tags.shiny.appobj <- function(x, ...) {
|
||||
# jcheng 06/06/2014: Unfortunate copy/paste between this function and
|
||||
# knit_print.shiny.appobj, but I am trying to make the most conservative
|
||||
# change possible due to upcoming release.
|
||||
opts <- x$options %||% list()
|
||||
width <- if (is.null(opts$width)) "100%" else opts$width
|
||||
height <- if (is.null(opts$height)) "400" else opts$height
|
||||
|
||||
path <- addSubApp(x)
|
||||
deferredIFrame(path, width, height)
|
||||
}
|
||||
|
||||
# Generate subapp iframes in such a way that they will not actually load right
|
||||
# away. Loading subapps immediately upon app load can result in a storm of
|
||||
# connections, all of which are contending for the few concurrent connections
|
||||
# that a browser will make to a specific origin. Instead, we load dummy iframes
|
||||
# and let the client load them when convenient. (See the initIframes function in
|
||||
# init_shiny.js.)
|
||||
deferredIFrame <- function(path, width, height) {
|
||||
tags$iframe("data-deferred-src" = path,
|
||||
width = width, height = height,
|
||||
class = "shiny-frame shiny-frame-deferred"
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user