Merge branch 'main' into revert-3901-revert-3875-ENG-3009-test

This commit is contained in:
x032205
2025-07-03 02:54:28 -04:00
670 changed files with 11618 additions and 13525 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+365
View File
@@ -0,0 +1,365 @@
{
"v": "5.7.5",
"fr": 100,
"ip": 0,
"op": 100,
"w": 22,
"h": 20,
"nm": "Comp 1",
"ddd": 0,
"metadata": {},
"assets": [
{
"id": "0",
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "Path 1",
"sr": 1,
"ks": {
"p": { "a": 0, "k": [-6, -6], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"nm": "Path 1",
"it": [
{
"ty": "sh",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": false,
"v": [
[9.25, 9.25],
[12.25, 9.25]
],
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
]
}
}
},
{
"ty": "st",
"c": { "a": 0, "k": [1, 1, 1], "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"w": { "a": 0, "k": 1.5, "ix": 2 },
"lc": 2,
"lj": 2,
"ml": 4
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
}
],
"ip": 0,
"op": 101,
"st": 0,
"bm": 0
}
]
}
],
"layers": [
{
"ddd": 0,
"ind": 2,
"ty": 3,
"nm": "",
"sr": 1,
"ks": {
"p": { "a": 0, "k": [0.7886505126953125, 0.7357635498046875], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
},
"ao": 0,
"ip": 0,
"op": 101,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"refId": "0",
"w": 10,
"h": 7,
"ind": 3,
"ty": 0,
"nm": "10 Outlines 2",
"sr": 1,
"ks": {
"p": { "a": 0, "k": [6, 6], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": {
"a": 1,
"k": [
{ "t": 0, "s": [100], "i": { "x": [0.667], "y": [1] }, "o": { "x": [1], "y": [0] } },
{ "t": 50, "s": [0], "i": { "x": [0.667], "y": [1] }, "o": { "x": [1], "y": [0] } },
{
"t": 100,
"s": [100],
"i": { "x": [0.75], "y": [0.75] },
"o": { "x": [0.25], "y": [0.25] }
}
],
"ix": 2
},
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
},
"ao": 0,
"ip": 0,
"op": 101,
"st": 0,
"bm": 0,
"parent": 2
},
{
"ddd": 0,
"ind": 4,
"ty": 4,
"nm": "10 Outlines",
"sr": 1,
"ks": {
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "gr",
"it": [
{
"ty": "gr",
"nm": "Path 1",
"it": [
{
"ty": "sh",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": false,
"v": [
[4.75, 4.75],
[7.75, 7],
[4.75, 9.25]
],
"i": [
[0, 0],
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0],
[0, 0]
]
}
}
},
{
"ty": "st",
"c": { "a": 0, "k": [1, 1, 1], "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"w": { "a": 0, "k": 1, "ix": 2 },
"lc": 2,
"lj": 2,
"ml": 4
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "gr",
"it": [
{
"ty": "gr",
"nm": "Group",
"it": [
{
"ty": "gr",
"nm": "Path 2",
"it": [
{
"ty": "sh",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"v": [
[3.25, 17.5],
[16.75, 17.5],
[18.341, 16.841],
[19, 15.25],
[19, 3.25],
[18.341, 1.659],
[16.75, 1],
[3.25, 1],
[1.659, 1.659],
[1, 3.25],
[1, 15.25],
[1.659, 16.841],
[3.25, 17.5]
],
"i": [
[0, 0],
[0, 0],
[-0.422, 0.422],
[0, 0.597],
[0, 0],
[0.422, 0.422],
[0.597, 0],
[0, 0],
[0.422, -0.422],
[0, -0.597],
[0, 0],
[-0.422, -0.422],
[-0.597, 0]
],
"o": [
[0, 0],
[0.597, 0],
[0.422, -0.422],
[0, 0],
[0, -0.597],
[-0.422, -0.422],
[0, 0],
[-0.597, 0],
[-0.422, 0.422],
[0, 0],
[0, 0.597],
[0.422, 0.422],
[0, 0]
]
}
}
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "st",
"c": { "a": 0, "k": [1, 1, 1], "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"w": { "a": 0, "k": 1.5, "ix": 2 },
"lc": 2,
"lj": 2,
"ml": 4
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "tr",
"p": { "a": 0, "k": [0.7886505126953125, 0.7357635498046875], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
}
],
"ip": 0,
"op": 101,
"st": 0,
"bm": 0
}
],
"markers": []
}
+992
View File
@@ -0,0 +1,992 @@
{
"v": "5.7.5",
"fr": 100,
"ip": 0,
"op": 200,
"w": 500,
"h": 500,
"nm": "Comp 1",
"ddd": 0,
"metadata": {},
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "Shape Layer 2",
"sr": 1,
"ks": {
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"nm": "Shape Layer 2",
"it": [
{
"ty": "rc",
"d": 1,
"s": { "a": 0, "k": [49.3180325191623, 68.7788286222386], "ix": 2 },
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 }
},
{
"ty": "fl",
"c": { "a": 0, "k": [1, 1, 1], "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"r": 1,
"bm": 0
},
{
"ty": "tr",
"p": { "a": 0, "k": [36.65843963623047, 374.4748840332031], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
}
],
"ip": 0,
"op": 201,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"ind": 2,
"ty": 4,
"nm": "Shape Layer 1",
"sr": 1,
"ks": {
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"nm": "Shape Layer 1",
"it": [
{
"ty": "rc",
"d": 1,
"s": { "a": 0, "k": [49.3180325191623, 68.7788286222386], "ix": 2 },
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 }
},
{
"ty": "fl",
"c": { "a": 0, "k": [1, 1, 1], "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"r": 1,
"bm": 0
},
{
"ty": "tr",
"p": { "a": 0, "k": [36.65843963623047, 152], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
}
],
"ip": 0,
"op": 201,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"ind": 3,
"ty": 4,
"nm": "bank-safe-box-svgrepo-com.svg 1",
"sr": 1,
"ks": {
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "gr",
"nm": "bank-safe-box-svgrepo-com.svg 1",
"it": [
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "sh",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"v": [
[33.06575776130341, 49.665937523501114],
[20.000300594090934, 36.60048035628864],
[33.06575776130341, 23.530797074065426],
[46.12614359050298, 36.60048035628864],
[33.06575776130341, 49.665937523501114],
[33.06575776130341, 49.665937523501114]
],
"i": [
[0, 0],
[0, 7.20383564731219],
[-7.208906985325083, 0],
[0, -7.208061762322929],
[7.1996095323014515, 0],
[0, 0]
],
"o": [
[-7.20383564731219, 0],
[0, -7.20383564731219],
[7.1996095323014515, 0],
[0, 7.1996095323014515],
[0, 0],
[0, 0]
]
}
}
},
{
"ty": "tr",
"p": { "a": 0, "k": [-45.124942779541016, -76.91899108886719], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 1.36079623004602e-7, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "sh",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"v": [
[56.59403564377824, 2.417355107683346],
[49.427568347046794, 2.417355107683346],
[38.852034318840836, 13.001059012587813],
[29.055609441248567, 10.541183410268967],
[19.84890111807693, 12.677977524964414],
[8.650971028656253, 1.4845037319247485],
[1.4845037319248013, 1.4845037319247485],
[1.4845037319248013, 8.650971028656201],
[11.935261461462396, 19.101728758193797],
[7.4351448327033705, 32.15793443849666],
[11.935261461462396, 45.210426538482],
[1.5253531154174096, 55.624791180908],
[1.5253531154174096, 62.78680218125844],
[5.108215405751383, 64.27371974038958],
[8.691077696085358, 62.78680218125844],
[19.84444482169591, 51.63343505564789],
[29.051153144867545, 53.76651559002584],
[39.33182889586317, 51.04371850122726],
[49.17653031758308, 60.89213350326467],
[52.75939260791704, 62.3745947660148],
[56.34225489825104, 60.89213350326467],
[56.34225489825104, 53.72566620653322],
[46.913474472092304, 44.296885780374474],
[50.66196244458722, 32.15347814211567],
[46.56959693469084, 19.59786308861321],
[56.58363761888923, 9.583822404414821],
[56.59403564377824, 2.417355107683346],
[56.59403564377824, 2.417355107683346]
],
"i": [
[0, 0],
[1.979338309233005, -1.9756247289154982],
[3.5251780094019862, -3.527901301634823],
[3.5427556229048607, 0],
[2.8037531397203104, -1.333175333986213],
[3.732643363140226, 3.731157931013225],
[1.9793383092330157, -1.9793383092330104],
[-1.9793383092330157, -1.9793383092330157],
[-3.483585909845862, -3.483585909845862],
[0, -4.9204939207012],
[-2.779243509624741, -3.6363378469061254],
[3.469969448681659, -3.4714548808086705],
[-1.9793383092330157, -1.979338309233005],
[-1.296782246874607, 0],
[-0.9892977965847497, 0.9900405126482554],
[-3.7177890418701867, 3.717789041870177],
[-3.308800062901711, 0],
[-3.0696454904540436, 1.6807664517051837],
[-3.281567140573295, -3.2828050006791307],
[-1.2967822468746175, 0],
[-0.9892977965847708, 0.9892977965847708],
[1.979338309233005, 1.979338309233005],
[3.142926808719575, 3.1429268087195856],
[0, 4.496403048441521],
[2.5490015299390887, 3.5472119192858735],
[-3.3380135613994684, 3.3380135613994586],
[1.979338309233005, 1.983794605614028],
[0, 0]
],
"o": [
[-1.979338309233005, -1.9756247289154982],
[-3.5251780094019756, 3.527901301634823],
[-2.9530390684842214, -1.5233106462427428],
[-3.3043437665206987, 0],
[-3.732643363140226, -3.731157931013225],
[-1.9793383092330157, -1.9793383092330104],
[-1.9793383092330157, 1.9793383092330157],
[3.483585909845862, 3.483585909845862],
[-2.779243509624741, 3.6355951308426193],
[0, 4.9204939207012],
[-3.469969448681659, 3.47145488080866],
[-1.9793383092330157, 1.9756247289154982],
[0.9900405126482554, 0.9900405126482554],
[1.296782246874607, 0],
[3.7177890418701867, -3.717789041870177],
[2.7992968433392984, 1.3287190376051898],
[3.741060811859921, 0],
[3.2815671405733053, 3.2828050006791307],
[0.9900405126482554, 0.9900405126482554],
[1.2967822468746175, 0],
[1.979338309233005, -1.979338309233005],
[-3.142926808719575, -3.142926808719575],
[2.3588662176825586, -3.46179957198314],
[0, -4.693965521333064],
[3.3380135613994684, -3.3380135613994586],
[1.9897363341220202, -1.9756247289155193],
[0, 0],
[0, 0]
]
}
}
},
{
"ty": "tr",
"p": { "a": 0, "k": [-41.346412658691406, -72.31075286865234], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 1.36079623004602e-7, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "tr",
"p": { "a": 0, "k": [563.65673828125, 513.2783813476562], "ix": 2 },
"a": { "a": 0, "k": [-12.307790756225586, -40.173892974853516], "ix": 2 },
"s": { "a": 0, "k": [366.3825750350952, 366.3825750350952], "ix": 2 },
"r": {
"a": 1,
"k": [
{
"t": 0,
"s": [0],
"i": { "x": [0.75], "y": [0.75] },
"o": { "x": [0.25], "y": [0.25] }
},
{
"t": 100,
"s": [180],
"i": { "x": [0.75], "y": [0.75] },
"o": { "x": [0.25], "y": [0.25] }
}
],
"ix": 2
},
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "sh",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"v": [
[16.204001519577492, 10.809684651948828],
[107.78162573450534, 10.809684651948828],
[107.78162573450534, 103.16527938802837],
[16.204001519577492, 103.16527938802837],
[16.204001519577492, 10.809684651948828],
[16.204001519577492, 10.809684651948828]
],
"i": [
[0, 0],
[-30.52587473830928, 0],
[0, -30.785198245359847],
[30.52587473830929, 0],
[0, 30.785198245359847],
[0, 0]
],
"o": [
[30.52587473830928, 0],
[0, 30.785198245359833],
[-30.52587473830928, 0],
[0, -30.785198245359847],
[0, 0],
[0, 0]
]
}
}
},
{
"ty": "tr",
"p": { "a": 0, "k": [-33.81110763549805, -68.36682891845703], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "sh",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"v": [
[5.187128475098463, 75.41686707022457],
[0.11734912644122932, 75.41686707022457],
[0.11734912644122932, 85.55642576753904],
[5.187128475098463, 85.55642576753904],
[5.187128475098463, 101.79962447177776],
[10.253194243805027, 106.869403820435],
[106.98749566286637, 106.869403820435],
[112.05356143157292, 101.79962447177776],
[112.05356143157292, 5.066065768706561],
[106.98749566286637, 0],
[10.2569078237557, 0],
[5.190842055049137, 5.066065768706561],
[5.190842055049137, 21.277327685369535],
[0, 21.277327685369535],
[0, 31.413172802733335],
[5.190842055049137, 31.413172802733335],
[5.190842055049137, 75.4176097862147],
[5.187128475098463, 75.4176097862147],
[5.187128475098463, 75.41686707022457]
],
"i": [
[0, 0],
[1.6899264495524133, 0],
[0, -3.3798528991048267],
[-1.6899264495524133, 0],
[0, -5.414399568079578],
[-2.795582986865779, 0],
[-32.24476713968711, 0],
[0, 2.799296566816458],
[0, 32.244519567690396],
[2.7955829868657895, 0],
[32.243529279703566, 0],
[0, -2.800039282806585],
[0, -5.40375397222099],
[1.7302806850163799, 0],
[0, -3.3786150391212706],
[-1.7302806850163799, 0],
[0, -14.66814566116046],
[0.0012378599835561636, 0],
[0, 0.0002475719967196764]
],
"o": [
[-1.6899264495524133, 0],
[0, 3.3798528991048267],
[1.6899264495524107, 0],
[0, 5.414399568079578],
[0, 2.799296566816458],
[32.24476713968713, 0],
[2.799296566816458, 0],
[0, -32.2445195676904],
[0, -2.799296566816452],
[-32.24352927970354, 0],
[-2.799296566816452, 0],
[0, 5.40375397222099],
[-1.7302806850163799, 0],
[0, 3.3786150391212706],
[1.7302806850163799, 0],
[0, 14.66814566116045],
[-0.0012378599835561636, 0],
[0, -0.0002475719967196764],
[0, 0]
]
}
}
},
{
"ty": "tr",
"p": { "a": 0, "k": [-30.517711639404297, -64.98767852783203], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "tr",
"p": { "a": 0, "k": [738.9981689453125, 819], "ix": 2 },
"a": { "a": 0, "k": [75.43499755859375, 71.94499969482422], "ix": 2 },
"s": { "a": 0, "k": [366.3825750350952, 366.3825750350952], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "tr",
"p": { "a": 0, "k": [360.7227783203125, 392.2637023925781], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [454.6020030975342, 454.6020030975342], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "fl",
"c": { "a": 0, "k": [1, 1, 1], "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"r": 1,
"bm": 0
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "gr",
"it": [
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "sh",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"v": [
[33.06575776130341, 49.665937523501114],
[20.000300594090934, 36.60048035628864],
[33.06575776130341, 23.530797074065426],
[46.12614359050298, 36.60048035628864],
[33.06575776130341, 49.665937523501114],
[33.06575776130341, 49.665937523501114]
],
"i": [
[0, 0],
[0, 7.20383564731219],
[-7.208906985325083, 0],
[0, -7.208061762322929],
[7.1996095323014515, 0],
[0, 0]
],
"o": [
[-7.20383564731219, 0],
[0, -7.20383564731219],
[7.1996095323014515, 0],
[0, 7.1996095323014515],
[0, 0],
[0, 0]
]
}
}
},
{
"ty": "tr",
"p": { "a": 0, "k": [-45.124942779541016, -76.91899108886719], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 1.36079623004602e-7, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "sh",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"v": [
[56.59403564377824, 2.417355107683346],
[49.427568347046794, 2.417355107683346],
[38.852034318840836, 13.001059012587813],
[29.055609441248567, 10.541183410268967],
[19.84890111807693, 12.677977524964414],
[8.650971028656253, 1.4845037319247485],
[1.4845037319248013, 1.4845037319247485],
[1.4845037319248013, 8.650971028656201],
[11.935261461462396, 19.101728758193797],
[7.4351448327033705, 32.15793443849666],
[11.935261461462396, 45.210426538482],
[1.5253531154174096, 55.624791180908],
[1.5253531154174096, 62.78680218125844],
[5.108215405751383, 64.27371974038958],
[8.691077696085358, 62.78680218125844],
[19.84444482169591, 51.63343505564789],
[29.051153144867545, 53.76651559002584],
[39.33182889586317, 51.04371850122726],
[49.17653031758308, 60.89213350326467],
[52.75939260791704, 62.3745947660148],
[56.34225489825104, 60.89213350326467],
[56.34225489825104, 53.72566620653322],
[46.913474472092304, 44.296885780374474],
[50.66196244458722, 32.15347814211567],
[46.56959693469084, 19.59786308861321],
[56.58363761888923, 9.583822404414821],
[56.59403564377824, 2.417355107683346],
[56.59403564377824, 2.417355107683346]
],
"i": [
[0, 0],
[1.979338309233005, -1.9756247289154982],
[3.5251780094019862, -3.527901301634823],
[3.5427556229048607, 0],
[2.8037531397203104, -1.333175333986213],
[3.732643363140226, 3.731157931013225],
[1.9793383092330157, -1.9793383092330104],
[-1.9793383092330157, -1.9793383092330157],
[-3.483585909845862, -3.483585909845862],
[0, -4.9204939207012],
[-2.779243509624741, -3.6363378469061254],
[3.469969448681659, -3.4714548808086705],
[-1.9793383092330157, -1.979338309233005],
[-1.296782246874607, 0],
[-0.9892977965847497, 0.9900405126482554],
[-3.7177890418701867, 3.717789041870177],
[-3.308800062901711, 0],
[-3.0696454904540436, 1.6807664517051837],
[-3.281567140573295, -3.2828050006791307],
[-1.2967822468746175, 0],
[-0.9892977965847708, 0.9892977965847708],
[1.979338309233005, 1.979338309233005],
[3.142926808719575, 3.1429268087195856],
[0, 4.496403048441521],
[2.5490015299390887, 3.5472119192858735],
[-3.3380135613994684, 3.3380135613994586],
[1.979338309233005, 1.983794605614028],
[0, 0]
],
"o": [
[-1.979338309233005, -1.9756247289154982],
[-3.5251780094019756, 3.527901301634823],
[-2.9530390684842214, -1.5233106462427428],
[-3.3043437665206987, 0],
[-3.732643363140226, -3.731157931013225],
[-1.9793383092330157, -1.9793383092330104],
[-1.9793383092330157, 1.9793383092330157],
[3.483585909845862, 3.483585909845862],
[-2.779243509624741, 3.6355951308426193],
[0, 4.9204939207012],
[-3.469969448681659, 3.47145488080866],
[-1.9793383092330157, 1.9756247289154982],
[0.9900405126482554, 0.9900405126482554],
[1.296782246874607, 0],
[3.7177890418701867, -3.717789041870177],
[2.7992968433392984, 1.3287190376051898],
[3.741060811859921, 0],
[3.2815671405733053, 3.2828050006791307],
[0.9900405126482554, 0.9900405126482554],
[1.2967822468746175, 0],
[1.979338309233005, -1.979338309233005],
[-3.142926808719575, -3.142926808719575],
[2.3588662176825586, -3.46179957198314],
[0, -4.693965521333064],
[3.3380135613994684, -3.3380135613994586],
[1.9897363341220202, -1.9756247289155193],
[0, 0],
[0, 0]
]
}
}
},
{
"ty": "tr",
"p": { "a": 0, "k": [-41.346412658691406, -72.31075286865234], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 1.36079623004602e-7, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "fl",
"c": { "a": 0, "k": [0.00392156862745098, 0, 0.00784313725490196], "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"r": 1,
"bm": 0,
"hd": true
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "tr",
"p": { "a": 0, "k": [563.65673828125, 513.2783813476562], "ix": 2 },
"a": { "a": 0, "k": [-12.307790756225586, -40.173892974853516], "ix": 2 },
"s": { "a": 0, "k": [366.3825750350952, 366.3825750350952], "ix": 2 },
"r": {
"a": 1,
"k": [
{
"t": 0,
"s": [0],
"i": { "x": [0.75], "y": [0.75] },
"o": { "x": [0.25], "y": [0.25] }
},
{
"t": 100,
"s": [180],
"i": { "x": [0.75], "y": [0.75] },
"o": { "x": [0.25], "y": [0.25] }
}
],
"ix": 2
},
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "gr",
"it": [
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "sh",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"v": [
[16.204001519577492, 10.809684651948828],
[107.78162573450534, 10.809684651948828],
[107.78162573450534, 103.16527938802837],
[16.204001519577492, 103.16527938802837],
[16.204001519577492, 10.809684651948828],
[16.204001519577492, 10.809684651948828]
],
"i": [
[0, 0],
[-30.52587473830928, 0],
[0, -30.785198245359847],
[30.52587473830929, 0],
[0, 30.785198245359847],
[0, 0]
],
"o": [
[30.52587473830928, 0],
[0, 30.785198245359833],
[-30.52587473830928, 0],
[0, -30.785198245359847],
[0, 0],
[0, 0]
]
}
}
},
{
"ty": "tr",
"p": { "a": 0, "k": [-33.81110763549805, -68.36682891845703], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "sh",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"v": [
[5.187128475098463, 75.41686707022457],
[0.11734912644122932, 75.41686707022457],
[0.11734912644122932, 85.55642576753904],
[5.187128475098463, 85.55642576753904],
[5.187128475098463, 101.79962447177776],
[10.253194243805027, 106.869403820435],
[106.98749566286637, 106.869403820435],
[112.05356143157292, 101.79962447177776],
[112.05356143157292, 5.066065768706561],
[106.98749566286637, 0],
[10.2569078237557, 0],
[5.190842055049137, 5.066065768706561],
[5.190842055049137, 21.277327685369535],
[0, 21.277327685369535],
[0, 31.413172802733335],
[5.190842055049137, 31.413172802733335],
[5.190842055049137, 75.4176097862147],
[5.187128475098463, 75.4176097862147],
[5.187128475098463, 75.41686707022457]
],
"i": [
[0, 0],
[1.6899264495524133, 0],
[0, -3.3798528991048267],
[-1.6899264495524133, 0],
[0, -5.414399568079578],
[-2.795582986865779, 0],
[-32.24476713968711, 0],
[0, 2.799296566816458],
[0, 32.244519567690396],
[2.7955829868657895, 0],
[32.243529279703566, 0],
[0, -2.800039282806585],
[0, -5.40375397222099],
[1.7302806850163799, 0],
[0, -3.3786150391212706],
[-1.7302806850163799, 0],
[0, -14.66814566116046],
[0.0012378599835561636, 0],
[0, 0.0002475719967196764]
],
"o": [
[-1.6899264495524133, 0],
[0, 3.3798528991048267],
[1.6899264495524107, 0],
[0, 5.414399568079578],
[0, 2.799296566816458],
[32.24476713968713, 0],
[2.799296566816458, 0],
[0, -32.2445195676904],
[0, -2.799296566816452],
[-32.24352927970354, 0],
[-2.799296566816452, 0],
[0, 5.40375397222099],
[-1.7302806850163799, 0],
[0, 3.3786150391212706],
[1.7302806850163799, 0],
[0, 14.66814566116045],
[-0.0012378599835561636, 0],
[0, -0.0002475719967196764],
[0, 0]
]
}
}
},
{
"ty": "tr",
"p": { "a": 0, "k": [-30.517711639404297, -64.98767852783203], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "fl",
"c": { "a": 0, "k": [0.00392156862745098, 0, 0.00784313725490196], "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"r": 1,
"bm": 0,
"hd": true
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "tr",
"p": { "a": 0, "k": [738.9981689453125, 819], "ix": 2 },
"a": { "a": 0, "k": [75.43499755859375, 71.94499969482422], "ix": 2 },
"s": { "a": 0, "k": [366.3825750350952, 366.3825750350952], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "gr",
"it": [
{
"ty": "gr",
"nm": "Path",
"it": [
{
"ty": "fl",
"c": { "a": 0, "k": [0.00392156862745098, 0, 0.00784313725490196], "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"r": 1,
"bm": 0,
"hd": true
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [100, 100], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "tr",
"p": { "a": 0, "k": [360.7227783203125, 392.2637023925781], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 2 },
"s": { "a": 0, "k": [454.6020030975342, 454.6020030975342], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
},
{
"ty": "tr",
"p": { "a": 0, "k": [80.75597381591797, 122.56097412109375], "ix": 2 },
"a": { "a": 0, "k": [399.99816274642944, 399.99998253828016], "ix": 2 },
"s": { "a": 0, "k": [112.70011646566, 112.70011646566], "ix": 2 },
"r": { "a": 0, "k": 0, "ix": 2 },
"o": { "a": 0, "k": 100, "ix": 2 },
"sk": { "a": 0, "k": 0, "ix": 2 },
"sa": { "a": 0, "k": 0, "ix": 2 }
}
]
}
],
"ip": 0,
"op": 201,
"st": 0,
"bm": 0
}
],
"markers": []
}
@@ -4,7 +4,6 @@ import { useNavigate } from "@tanstack/react-router";
import { useAddUsersToOrg } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { Button, EmailServiceSetupModal } from "../v2";
@@ -23,7 +22,7 @@ export default function TeamInviteStep(): JSX.Element {
// Redirect user to the getting started page
const redirectToHome = async () => {
navigate({ to: `/organization/${ProjectType.SecretManager}/overview` as const });
navigate({ to: "/organization/projects" as const });
};
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
@@ -71,7 +71,7 @@ export default function NavHeader({
{currentOrg?.name?.charAt(0)}
</div>
<Link
to="/organization/secret-manager/overview"
to="/organization/projects"
className="truncate pl-0.5 text-sm font-semibold text-primary/80 hover:text-primary"
>
{currentOrg?.name}
@@ -93,7 +93,7 @@ export default function NavHeader({
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
{pageName === "Secrets" ? (
<Link
to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
to="/projects/$projectId/secret-manager/overview"
params={{ projectId: currentWorkspace.id }}
className="text-sm font-semibold text-primary/80 hover:text-primary"
>
@@ -129,7 +129,7 @@ export default function NavHeader({
<div className="flex items-center space-x-3">
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
<Link
to={`/${ProjectType.SecretManager}/$projectId/secrets/$envSlug` as const}
to={`/projects/$projectId/${ProjectType.SecretManager}/secrets/$envSlug` as const}
params={{ projectId: currentWorkspace.id, envSlug: routerEnvSlug }}
className="text-sm font-semibold text-primary/80 hover:text-primary"
>
@@ -191,7 +191,7 @@ export default function NavHeader({
</div>
) : (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secrets/$envSlug` as const}
to={`/projects/$projectId/${ProjectType.SecretManager}/secrets/$envSlug` as const}
params={{
projectId: currentWorkspace.id,
envSlug: routerEnvSlug || ""
@@ -70,7 +70,7 @@ export const SecretDashboardPathBreadcrumb = ({
</div>
) : (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secrets/$envSlug` as const}
to={`/projects/$projectId/${ProjectType.SecretManager}/secrets/$envSlug` as const}
params={{
projectId,
envSlug: environmentSlug
@@ -65,6 +65,7 @@ export const createNotification = (
toast(<NotificationContent {...myProps} />, {
position: "bottom-right",
...toastProps,
autoClose: toastProps.autoClose || 15000,
theme: "dark",
type: myProps?.type || "info"
});
@@ -7,7 +7,6 @@ import z from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { GenericResourceNameSchema } from "@app/lib/schemas";
const schema = z
@@ -59,10 +58,7 @@ export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) =>
});
navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const,
params: {
organizationId: organization.id
}
to: "/organization/projects"
});
localStorage.setItem("orgData.id", organization.id);
@@ -5,12 +5,14 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useUpdateProject } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
const baseFormSchema = z.object({
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
defaultProduct: z.nativeEnum(ProjectType).default(ProjectType.SecretManager),
description: z
.string()
.trim()
@@ -50,6 +52,7 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
reset({
name: currentWorkspace.name,
description: currentWorkspace.description ?? "",
defaultProduct: currentWorkspace.defaultProduct,
...(showSlugField && { slug: currentWorkspace.slug })
});
}
@@ -63,6 +66,7 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
projectID: currentWorkspace.id,
newProjectName: data.name,
newProjectDescription: data.description,
defaultProduct: data.defaultProduct,
...(showSlugField &&
"slug" in data && {
newSlug: data.slug !== currentWorkspace.slug ? data.slug : undefined
@@ -212,6 +216,31 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
</ProjectPermissionCan>
</div>
</div>
<Controller
control={control}
name="defaultProduct"
defaultValue={ProjectType.SecretManager}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Default Product"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full max-w-md bg-mineshaft-800"
>
<SelectItem value={ProjectType.SecretManager}>Secrets Manager</SelectItem>
<SelectItem value={ProjectType.CertificateManager}>PKI Manager</SelectItem>
<SelectItem value={ProjectType.KMS}>KMS</SelectItem>
<SelectItem value={ProjectType.SSH}>SSH</SelectItem>
<SelectItem value={ProjectType.SecretScanning}>Secret Scanning</SelectItem>
</Select>
</FormControl>
)}
/>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
@@ -35,7 +35,6 @@ import { getProjectHomePage } from "@app/helpers/project";
import { useCreateWorkspace, useGetExternalKmsList, useGetUserWorkspaces } from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
import { ProjectType } from "@app/hooks/api/workspace/types";
const formSchema = z.object({
name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
@@ -53,12 +52,11 @@ type TAddProjectFormData = z.infer<typeof formSchema>;
interface NewProjectModalProps {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
projectType: ProjectType;
}
type NewProjectFormProps = Pick<NewProjectModalProps, "onOpenChange" | "projectType">;
type NewProjectFormProps = Pick<NewProjectModalProps, "onOpenChange">;
const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const { permission } = useOrgPermission();
@@ -72,7 +70,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
OrgPermissionSubjects.ProjectTemplates
);
const { data: projectTemplates = [] } = useListProjectTemplates(projectType, {
const { data: projectTemplates = [] } = useListProjectTemplates({
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
});
@@ -115,15 +113,17 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
projectName: name,
projectDescription: description,
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
template,
type: projectType
template
});
await refetchWorkspaces();
createNotification({ text: "Project created", type: "success" });
reset();
onOpenChange(false);
navigate({ to: getProjectHomePage(project), params: { projectId: project.id } });
navigate({
to: getProjectHomePage(project.defaultProduct),
params: { projectId: project.id }
});
} catch (err) {
console.error(err);
createNotification({ text: "Failed to create project", type: "error" });
@@ -270,18 +270,14 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
);
};
export const NewProjectModal: FC<NewProjectModalProps> = ({
isOpen,
onOpenChange,
projectType
}) => {
export const NewProjectModal: FC<NewProjectModalProps> = ({ isOpen, onOpenChange }) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Create a new project"
subTitle="This project will contain your secrets and configurations."
>
<NewProjectForm onOpenChange={onOpenChange} projectType={projectType} />
<NewProjectForm onOpenChange={onOpenChange} />
</ModalContent>
</Modal>
);
@@ -1,30 +0,0 @@
import { useState } from "react";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectTemplatesTab } from "./components";
const tabs = [
{ name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab }
];
export const ProjectSettings = () => {
const [selectedTab, setSelectedTab] = useState(tabs[0].key);
return (
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
</TabList>
{tabs.map(({ key, component: Component }) => (
<TabPanel value={key} key={`tab-panel-${key}`}>
<Component />
</TabPanel>
))}
</Tabs>
);
};
@@ -1 +0,0 @@
export * from "./ProjectTemplatesTab";
@@ -1 +0,0 @@
export * from "./ProjectSettings";
@@ -25,6 +25,7 @@ import { TeamCitySyncFields } from "./TeamCitySyncFields";
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
import { VercelSyncFields } from "./VercelSyncFields";
import { WindmillSyncFields } from "./WindmillSyncFields";
import { ZabbixSyncFields } from "./ZabbixSyncFields";
export const SecretSyncDestinationFields = () => {
const { watch } = useFormContext<TSecretSyncForm>();
@@ -76,6 +77,8 @@ export const SecretSyncDestinationFields = () => {
return <GitLabSyncFields />;
case SecretSync.CloudflarePages:
return <CloudflarePagesSyncFields />;
case SecretSync.Zabbix:
return <ZabbixSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}
@@ -0,0 +1,147 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
import {
TZabbixHost,
useZabbixConnectionListHosts,
ZABBIX_SYNC_SCOPES,
ZabbixMacroType,
ZabbixSyncScope
} from "@app/hooks/api/appConnections/zabbix";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const ZabbixSyncFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Zabbix }
>();
const connectionId = useWatch({ name: "connection.id", control });
const currentScope = watch("destinationConfig.scope");
const { data: hosts = [], isPending: isHostsPending } = useZabbixConnectionListHosts(
connectionId,
{
enabled: Boolean(connectionId)
}
);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.scope", ZabbixSyncScope.Global);
setValue("destinationConfig.hostId", "");
setValue("destinationConfig.hostName", "");
}}
/>
<Controller
name="destinationConfig.scope"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Scope"
tooltipClassName="max-w-lg py-3"
tooltipText={
<div className="flex flex-col gap-3">
<p>
Specify how Infisical should manage secrets from Zabbix. The following options are
available:
</p>
<ul className="flex list-disc flex-col gap-3 pl-4">
{Object.values(ZABBIX_SYNC_SCOPES).map(({ name, description }) => {
return (
<li key={name}>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">{name}</span>: {description}
</p>
</li>
);
})}
</ul>
</div>
}
>
<Select
value={value}
onValueChange={(val) => {
onChange(val);
setValue("destinationConfig.hostId", "");
setValue("destinationConfig.hostName", "");
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select a scope..."
dropdownContainerClassName="max-w-none"
>
{Object.values(ZabbixSyncScope).map((scope) => (
<SelectItem className="capitalize" value={scope} key={scope}>
{scope.replace("-", " ")}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{currentScope === ZabbixSyncScope.Host && (
<Controller
name="destinationConfig.hostId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Host">
<FilterableSelect
menuPlacement="top"
isLoading={isHostsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={hosts.find((host) => host.hostId === value) ?? null}
onChange={(option) => {
const selectedOption = option as SingleValue<TZabbixHost>;
onChange(selectedOption?.hostId ?? null);
if (selectedOption) {
setValue("destinationConfig.hostName", selectedOption.host);
} else {
setValue("destinationConfig.hostName", "");
}
}}
options={hosts}
placeholder="Select a host..."
getOptionLabel={(option) => option.host}
getOptionValue={(option) => option.hostId}
/>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="destinationConfig.macroType"
defaultValue={ZabbixMacroType.Secret}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Macro Type">
<Select
value={String(value)}
onValueChange={(val) => onChange(parseInt(val, 10))}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select a macro type..."
dropdownContainerClassName="max-w-none"
>
<SelectItem value={String(ZabbixMacroType.Text)} key="text">
Text
</SelectItem>
<SelectItem value={String(ZabbixMacroType.Secret)} key="secret">
Secret
</SelectItem>
</Select>
</FormControl>
)}
/>
</>
);
};
@@ -23,6 +23,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
const { control, watch } = useFormContext<TSecretSyncForm>();
const destination = watch("destination");
const currentSyncOption = watch("syncOptions");
const destinationName = SECRET_SYNC_MAP[destination].name;
@@ -57,6 +58,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Flyio:
case SecretSync.GitLab:
case SecretSync.CloudflarePages:
case SecretSync.Zabbix:
AdditionalSyncOptionsFieldsComponent = null;
break;
default:
@@ -127,8 +129,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
{!syncOption?.canImportSecrets && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faTriangleExclamation} />
{destinationName} only supports overwriting destination secrets. Secrets not present
in Infisical will be removed from the destination.
{destinationName} only supports overwriting destination secrets.{" "}
{!currentSyncOption.disableSecretDeletion &&
"Secrets not present in Infisical will be removed from the destination."}
</p>
)}
</>
@@ -35,6 +35,7 @@ import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
import { WindmillSyncReviewFields } from "./WindmillSyncReviewFields";
import { ZabbixSyncReviewFields } from "./ZabbixSyncReviewFields";
export const SecretSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm>();
@@ -124,6 +125,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.CloudflarePages:
DestinationFieldsComponent = <CloudflarePagesSyncReviewFields />;
break;
case SecretSync.Zabbix:
DestinationFieldsComponent = <ZabbixSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}
@@ -0,0 +1,31 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { ZabbixSyncScope } from "@app/hooks/api/appConnections/zabbix";
import { SecretSync } from "@app/hooks/api/secretSyncs";
const isTextMacro = (macroType: number) => macroType === 0;
export const ZabbixSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Zabbix }>();
const scope = watch("destinationConfig.scope");
const hostId = watch("destinationConfig.hostId");
const hostName = watch("destinationConfig.hostName");
const macroType = watch("destinationConfig.macroType");
return (
<>
<GenericFieldLabel label="Scope">{scope}</GenericFieldLabel>
{scope === ZabbixSyncScope.Host && (
<>
<GenericFieldLabel label="Host ID">{hostId}</GenericFieldLabel>
<GenericFieldLabel label="Host Name">{hostName}</GenericFieldLabel>
</>
)}
<GenericFieldLabel label="Macro Type">
{isTextMacro(macroType) ? "Text" : "Secret"}
</GenericFieldLabel>
</>
);
};
@@ -22,6 +22,7 @@ import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schem
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema";
import { WindmillSyncDestinationSchema } from "./windmill-sync-destination-schema";
import { ZabbixSyncDestinationSchema } from "./zabbix-sync-destination-schema";
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema,
@@ -45,7 +46,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
RenderSyncDestinationSchema,
FlyioSyncDestinationSchema,
GitlabSyncDestinationSchema,
CloudflarePagesSyncDestinationSchema
CloudflarePagesSyncDestinationSchema,
ZabbixSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema;
@@ -0,0 +1,27 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { ZabbixMacroType, ZabbixSyncScope } from "@app/hooks/api/appConnections/zabbix";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const ZabbixSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.Zabbix),
destinationConfig: z.discriminatedUnion("scope", [
z.object({
scope: z.literal(ZabbixSyncScope.Host),
hostId: z.string().trim().min(1, "Host ID required"),
hostName: z.string().trim().min(1, "Host name required"),
macroType: z.nativeEnum(ZabbixMacroType, {
errorMap: () => ({ message: "Macro type must be either 'text' or 'secret'" })
})
}),
z.object({
scope: z.literal(ZabbixSyncScope.Global),
macroType: z.nativeEnum(ZabbixMacroType, {
errorMap: () => ({ message: "Macro type must be either 'text' or 'secret'" })
})
})
])
})
);
@@ -0,0 +1,20 @@
import { ComponentType, ReactNode } from "react";
type ShouldWrapProps<T extends Record<string, any>> = {
children: ReactNode;
wrapper: ComponentType<T & { children: ReactNode }>;
isWrapped?: boolean;
} & T;
export const ShouldWrap = <T extends Record<string, any>>({
children,
wrapper: Wrapper,
isWrapped = false,
...wrapperProps
}: ShouldWrapProps<T>) => {
if (isWrapped) {
return <Wrapper {...(wrapperProps as any)}>{children}</Wrapper>;
}
return children;
};
@@ -1,6 +1,6 @@
/* eslint-disable react/prop-types */
import React from "react";
import { faCaretDown, faChevronRight, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { faEllipsis, faSort } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, ReactNode } from "@tanstack/react-router";
import { LinkComponentProps } from "node_modules/@tanstack/react-router/dist/esm/link";
@@ -40,7 +40,7 @@ const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWitho
({ className, ...props }, ref) => (
<li
ref={ref}
className={twMerge("inline-flex items-center gap-1.5 font-medium", className)}
className={twMerge("inline-flex items-center gap-1.5 text-sm", className)}
{...props}
/>
)
@@ -78,13 +78,8 @@ const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWit
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={twMerge("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
{...props}
>
{children ?? <FontAwesomeIcon icon={faChevronRight} />}
<li role="presentation" aria-hidden="true" className={twMerge("", className)} {...props}>
{children ?? <p className="px-2 text-lg text-mineshaft-400/70">/</p>}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
@@ -139,12 +134,12 @@ const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[]
<DropdownMenu>
<DropdownMenuTrigger>
<BreadcrumbItem>
<BreadcrumbSegment>
{el.label} <FontAwesomeIcon icon={faCaretDown} size="sm" />
<BreadcrumbSegment className="rounded-md px-2 py-1 py-2 hover:bg-mineshaft-600">
{el.label} <FontAwesomeIcon icon={faSort} size="sm" />
</BreadcrumbSegment>
</BreadcrumbItem>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuContent side="right" align="start">
{el?.dropdownTitle && <DropdownMenuLabel>{el.dropdownTitle}</DropdownMenuLabel>}
{el.links.map((i, dropIndex) => (
<Link
+10 -5
View File
@@ -2,6 +2,8 @@ import { ButtonHTMLAttributes, forwardRef, ReactNode } from "react";
import { cva, VariantProps } from "cva";
import { twMerge } from "tailwind-merge";
import { Lottie } from "../Lottie";
type Props = {
children: ReactNode;
isDisabled?: boolean;
@@ -189,11 +191,14 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
{...props}
>
{isLoading && (
<img
src="/images/loading/loading.gif"
width={36}
alt="loading animation"
className="absolute rounded-xl opacity-80"
<Lottie
icon={
variant === "solid" && colorSchema === "primary"
? "infisical_loading_bw"
: "infisical_loading"
}
isAutoPlay
className="absolute w-8 rounded-xl opacity-80 shadow-sm"
/>
)}
{leftIcon && (
@@ -5,6 +5,8 @@ import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Lottie } from "../Lottie";
type Props = {
text?: string | string[];
frequency?: number;
@@ -31,14 +33,7 @@ export const ContentLoader = ({ text, frequency = 2000, className }: Props) => {
className
)}
>
<img
src="/images/loading/loading.gif"
height={70}
width={120}
alt="loading animation"
decoding="async"
loading="lazy"
/>
<Lottie icon="infisical_loading" className="h-32 w-32" />
{text && isTextArray && (
<AnimatePresence mode="wait">
<motion.div
@@ -20,6 +20,7 @@ type Props = {
children?: ReactNode;
deletionMessage?: ReactNode;
buttonColorSchema?: "danger" | "primary" | "secondary" | "gray" | null;
isDisabled?: boolean;
};
export const DeleteActionModal = ({
@@ -34,6 +35,7 @@ export const DeleteActionModal = ({
formContent,
deletionMessage,
buttonColorSchema = "danger",
isDisabled,
children
}: Props): JSX.Element => {
const [inputData, setInputData] = useState("");
@@ -70,7 +72,7 @@ export const DeleteActionModal = ({
<Button
className="mr-4"
colorSchema={buttonColorSchema}
isDisabled={!(deleteKey === inputData) || isLoading}
isDisabled={!(deleteKey === inputData) || isLoading || isDisabled}
onClick={onDelete}
isLoading={isLoading}
>
@@ -7,7 +7,7 @@ interface IProps {
export const Divider = ({ className }: IProps): JSX.Element => {
return (
<div className={twMerge("flex items-center px-2 opacity-50", className)}>
<div aria-hidden="true" className="h-5 w-full grow border border-t border-mineshaft-200" />
<div aria-hidden="true" className="h-1 w-full grow border-t border-mineshaft-300" />
</div>
);
};
@@ -0,0 +1,36 @@
import { MouseEventHandler } from "react";
export const HeaderResizer = ({
onMouseDown,
isActive,
scrollOffset,
heightOffset
}: {
onMouseDown: MouseEventHandler<HTMLDivElement>;
isActive: boolean;
scrollOffset: number;
heightOffset: number;
}) => {
return (
<>
<div
tabIndex={-1}
role="button"
className={`absolute left-0 z-40 h-0.5 w-full cursor-ns-resize hover:bg-blue-400/20 ${
isActive ? "bg-blue-400/75" : "bg-transparent"
}`}
onMouseDown={onMouseDown}
style={{
transform: "translateY(50%)",
top: heightOffset
}}
/>
<div
style={{ left: `calc(50% + ${scrollOffset}px)`, top: heightOffset }}
className="pointer-events-none absolute z-30 -translate-x-1/2"
>
<div className="h-1 w-8 rounded bg-gray-400 opacity-50" />
</div>
</>
);
};
@@ -0,0 +1,37 @@
import { forwardRef, ReactNode, useRef } from "react";
import { DotLottie, DotLottieReact, Mode } from "@lottiefiles/dotlottie-react";
export type LottieProps = {
// Kudos to https://itnext.io/react-polymorphic-components-with-typescript-f7ce72ea7af2
children?: ReactNode;
icon?: string;
iconMode?: Mode;
className?: string;
isAutoPlay?: boolean;
};
export const Lottie = forwardRef<HTMLDivElement, LottieProps>(
({ children, icon, iconMode, isAutoPlay, ...props }, ref): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null);
return (
<div
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
{...props}
ref={ref}
>
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
mode={iconMode}
src={`/lotties/${icon}.json`}
loop
autoplay={isAutoPlay}
className="h-full w-full"
/>
{children}
</div>
);
}
);
@@ -0,0 +1 @@
export { Lottie } from "./Lottie";
+12 -31
View File
@@ -1,5 +1,4 @@
import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react";
import { DotLottie, DotLottieReact, Mode } from "@lottiefiles/dotlottie-react";
import { ComponentPropsWithRef, ElementType, ReactNode, Ref } from "react";
import { twMerge } from "tailwind-merge";
export type MenuProps = {
@@ -14,9 +13,8 @@ export const Menu = ({ children, className }: MenuProps): JSX.Element => {
export type MenuItemProps<T extends ElementType> = {
// Kudos to https://itnext.io/react-polymorphic-components-with-typescript-f7ce72ea7af2
as?: T;
children: ReactNode;
icon?: string;
iconMode?: Mode;
children?: ReactNode;
leftIcon?: ReactNode;
description?: ReactNode;
isDisabled?: boolean;
isSelected?: boolean;
@@ -26,7 +24,7 @@ export type MenuItemProps<T extends ElementType> = {
export const MenuItem = <T extends ElementType = "button">({
children,
icon,
leftIcon,
iconMode,
className,
isDisabled,
@@ -37,41 +35,21 @@ export const MenuItem = <T extends ElementType = "button">({
inputRef,
...props
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null);
return (
<Item
type="button"
role="menuitem"
className={twMerge(
"duration-50 group relative mt-0.5 flex w-full cursor-pointer items-center rounded px-1 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700",
"duration-50 group relative mt-0.5 flex w-full cursor-pointer items-center rounded px-2 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700",
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
ref={inputRef}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
{...props}
>
<div
className={`${
isSelected ? "visisble" : "invisible"
} absolute -left-[0.28rem] h-5 w-[0.07rem] rounded-md bg-primary`}
/>
{icon && (
<div style={{ width: "22px", height: "22px" }} className="my-auto ml-1 mr-3">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
mode={iconMode}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
/>
</div>
)}
<span className="flex-grow text-left">{children}</span>
{leftIcon}
{children && <span className="flex-grow whitespace-nowrap text-left">{children}</span>}
{description && <span className="mt-2 text-xs">{description}</span>}
</Item>
);
@@ -82,11 +60,14 @@ MenuItem.displayName = "MenuItem";
export type MenuGroupProps = {
children: ReactNode;
title: ReactNode;
className?: string;
};
export const MenuGroup = ({ children, title }: MenuGroupProps): JSX.Element => (
export const MenuGroup = ({ children, title, className }: MenuGroupProps): JSX.Element => (
<>
<li className="px-2 pt-3 text-xs uppercase text-gray-400">{title}</li>
<li className={twMerge("px-2 pt-3 text-xs font-medium uppercase text-gray-400", className)}>
{title}
</li>
{children}
</>
);
+9 -3
View File
@@ -45,10 +45,14 @@ export const Table = ({ children, className }: TableProps): JSX.Element => (
export type THeadProps = {
children: ReactNode;
className?: string;
style?: React.CSSProperties;
};
export const THead = ({ children, className }: THeadProps): JSX.Element => (
<thead className={twMerge("bg-mineshaft-800 text-xs uppercase text-bunker-300", className)}>
export const THead = ({ children, className, style }: THeadProps): JSX.Element => (
<thead
className={twMerge("bg-mineshaft-800 text-xs uppercase text-bunker-300", className)}
style={style}
>
{children}
</thead>
);
@@ -96,14 +100,16 @@ export const Tr = ({
export type ThProps = {
children?: ReactNode;
className?: string;
style?: React.CSSProperties;
};
export const Th = ({ children, className }: ThProps): JSX.Element => (
export const Th = ({ children, className, style }: ThProps): JSX.Element => (
<th
className={twMerge(
"border-b-2 border-mineshaft-600 bg-mineshaft-800 px-5 pb-3.5 pt-4 font-semibold",
className
)}
style={style}
>
{children}
</th>
+2
View File
@@ -10,6 +10,7 @@ export * from "./ConfirmActionModal";
export * from "./ContentLoader";
export * from "./DatePicker";
export * from "./DeleteActionModal";
export * from "./Divider";
export * from "./Drawer";
export * from "./Dropdown";
export * from "./Editor";
@@ -22,6 +23,7 @@ export * from "./GenericFieldLabel";
export * from "./HoverCardv2";
export * from "./IconButton";
export * from "./Input";
export * from "./Lottie";
export * from "./Menu";
export * from "./Modal";
export * from "./NoticeBanner";
+132 -132
View File
@@ -74,276 +74,276 @@ export const ROUTE_PATHS = Object.freeze({
},
SecretManager: {
ApprovalPage: setRoute(
"/secret-manager/$projectId/approval",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/approval"
"/projects/$projectId/secret-manager/approval",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/approval"
),
SecretDashboardPage: setRoute(
"/secret-manager/$projectId/secrets/$envSlug",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug"
"/projects/$projectId/secret-manager/secrets/$envSlug",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/secrets/$envSlug"
),
RollbackPreviewPage: setRoute(
"/secret-manager/$projectId/commits/$environment/$folderId/$commitId/restore",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
"/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId/restore",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
),
CommitDetailsPage: setRoute(
"/secret-manager/$projectId/commits/$environment/$folderId/$commitId",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId"
"/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId"
),
CommitsPage: setRoute(
"/secret-manager/$projectId/commits/$environment/$folderId",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId"
"/projects/$projectId/secret-manager/commits/$environment/$folderId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId"
),
OverviewPage: setRoute(
"/secret-manager/$projectId/overview",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview"
"/projects/$projectId/secret-manager/overview",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/overview"
),
IntegrationsListPage: setRoute(
"/secret-manager/$projectId/integrations",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/"
"/projects/$projectId/secret-manager/integrations",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/"
),
IntegrationDetailsByIDPage: setRoute(
"/secret-manager/$projectId/integrations/$integrationId",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
"/projects/$projectId/secret-manager/integrations/$integrationId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/$integrationId"
),
SecretSyncDetailsByIDPage: setRoute(
"/secret-manager/$projectId/integrations/secret-syncs/$destination/$syncId",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId"
"/projects/$projectId/secret-manager/integrations/secret-syncs/$destination/$syncId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId"
),
Integratons: {
SelectIntegrationAuth: setRoute(
"/secret-manager/$projectId/integrations/select-integration-auth",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth"
"/projects/$projectId/secret-manager/integrations/select-integration-auth",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/select-integration-auth"
),
HerokuOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/heroku/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback"
"/projects/$projectId/secret-manager/integrations/heroku/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/heroku/oauth2/callback"
),
HerokuConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/heroku/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/create"
"/projects/$projectId/secret-manager/integrations/heroku/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/heroku/create"
),
AwsParameterStoreConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/aws-parameter-store/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create"
"/projects/$projectId/secret-manager/integrations/aws-parameter-store/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/aws-parameter-store/create"
),
AwsSecretManagerConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/aws-secret-manager/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create"
"/projects/$projectId/secret-manager/integrations/aws-secret-manager/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/aws-secret-manager/create"
),
AzureAppConfigurationsOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/azure-app-configuration/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
"/projects/$projectId/secret-manager/integrations/azure-app-configuration/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
),
AzureAppConfigurationsConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/azure-app-configuration/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create"
"/projects/$projectId/secret-manager/integrations/azure-app-configuration/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-app-configuration/create"
),
AzureDevopsConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/azure-devops/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/create"
"/projects/$projectId/secret-manager/integrations/azure-devops/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-devops/create"
),
AzureKeyVaultAuthorizePage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/authorize",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize"
"/projects/$projectId/secret-manager/integrations/azure-key-vault/authorize",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/authorize"
),
AzureKeyVaultOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
"/projects/$projectId/secret-manager/integrations/azure-key-vault/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
),
AzureKeyVaultConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/create"
"/projects/$projectId/secret-manager/integrations/azure-key-vault/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/create"
),
BitbucketOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/bitbucket/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
"/projects/$projectId/secret-manager/integrations/bitbucket/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
),
BitbucketConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/bitbucket/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/create"
"/projects/$projectId/secret-manager/integrations/bitbucket/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/bitbucket/create"
),
ChecklyConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/checkly/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/checkly/create"
"/projects/$projectId/secret-manager/integrations/checkly/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/checkly/create"
),
CircleConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/circleci/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/create"
"/projects/$projectId/secret-manager/integrations/circleci/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/circleci/create"
),
CloudflarePagesConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/cloudflare-pages/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create"
"/projects/$projectId/secret-manager/integrations/cloudflare-pages/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloudflare-pages/create"
),
DigitalOceanAppPlatformConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/digital-ocean-app-platform/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
"/projects/$projectId/secret-manager/integrations/digital-ocean-app-platform/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
),
CloudflareWorkersConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/cloudflare-workers/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create"
"/projects/$projectId/secret-manager/integrations/cloudflare-workers/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloudflare-workers/create"
),
CodefreshConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/codefresh/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/create"
"/projects/$projectId/secret-manager/integrations/codefresh/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/codefresh/create"
),
GcpSecretManagerConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/gcp-secret-manager/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create"
"/projects/$projectId/secret-manager/integrations/gcp-secret-manager/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gcp-secret-manager/create"
),
GcpSecretManagerOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/gcp-secret-manager/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
"/projects/$projectId/secret-manager/integrations/gcp-secret-manager/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
),
GithubConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/github/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/github/create"
"/projects/$projectId/secret-manager/integrations/github/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/github/create"
),
GithubOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/github/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/github/oauth2/callback"
"/projects/$projectId/secret-manager/integrations/github/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/github/oauth2/callback"
),
GitlabConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/gitlab/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/create"
"/projects/$projectId/secret-manager/integrations/gitlab/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gitlab/create"
),
GitlabOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/gitlab/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback"
"/projects/$projectId/secret-manager/integrations/gitlab/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gitlab/oauth2/callback"
),
VercelOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/vercel/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback"
"/projects/$projectId/secret-manager/integrations/vercel/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/vercel/oauth2/callback"
),
VercelConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/vercel/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/create"
"/projects/$projectId/secret-manager/integrations/vercel/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/vercel/create"
),
FlyioConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/flyio/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/flyio/create"
"/projects/$projectId/secret-manager/integrations/flyio/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/flyio/create"
),
HashicorpVaultConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/hashicorp-vault/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create"
"/projects/$projectId/secret-manager/integrations/hashicorp-vault/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/hashicorp-vault/create"
),
HasuraCloudConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/hasura-cloud/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/hasura-cloud/create"
"/projects/$projectId/secret-manager/integrations/hasura-cloud/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/hasura-cloud/create"
),
LaravelForgeConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/laravel-forge/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/laravel-forge/create"
"/projects/$projectId/secret-manager/integrations/laravel-forge/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/laravel-forge/create"
),
NorthflankConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/northflank/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/northflank/create"
"/projects/$projectId/secret-manager/integrations/northflank/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/northflank/create"
),
RailwayConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/railway/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/railway/create"
"/projects/$projectId/secret-manager/integrations/railway/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/railway/create"
),
RenderConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/render/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/render/create"
"/projects/$projectId/secret-manager/integrations/render/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/render/create"
),
RundeckConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/rundeck/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/rundeck/create"
"/projects/$projectId/secret-manager/integrations/rundeck/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/rundeck/create"
),
WindmillConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/windmill/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/create"
"/projects/$projectId/secret-manager/integrations/windmill/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/windmill/create"
),
TravisCIConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/travisci/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/travisci/create"
"/projects/$projectId/secret-manager/integrations/travisci/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/travisci/create"
),
TerraformCloudConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/terraform-cloud/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/terraform-cloud/create"
"/projects/$projectId/secret-manager/integrations/terraform-cloud/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/terraform-cloud/create"
),
TeamcityConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/teamcity/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/teamcity/create"
"/projects/$projectId/secret-manager/integrations/teamcity/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/teamcity/create"
),
SupabaseConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/supabase/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/supabase/create"
"/projects/$projectId/secret-manager/integrations/supabase/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/supabase/create"
),
OctopusDeployCloudConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/octopus-deploy/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/octopus-deploy/create"
"/projects/$projectId/secret-manager/integrations/octopus-deploy/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/octopus-deploy/create"
),
DatabricksConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/databricks/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/databricks/create"
"/projects/$projectId/secret-manager/integrations/databricks/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/databricks/create"
),
QoveryConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/qovery/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/qovery/create"
"/projects/$projectId/secret-manager/integrations/qovery/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/qovery/create"
),
Cloud66ConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/cloud-66/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/create"
"/projects/$projectId/secret-manager/integrations/cloud-66/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloud-66/create"
),
NetlifyConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/netlify/create",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/create"
"/projects/$projectId/secret-manager/integrations/netlify/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/netlify/create"
),
NetlifyOuathCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/netlify/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback"
"/projects/$projectId/secret-manager/integrations/netlify/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/netlify/oauth2/callback"
)
}
},
CertManager: {
CertAuthDetailsByIDPage: setRoute(
"/cert-manager/$projectId/ca/$caName",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName"
"/projects/$projectId/cert-manager/ca/$caName",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/ca/$caName"
),
SubscribersPage: setRoute(
"/cert-manager/$projectId/subscribers",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers"
"/projects/$projectId/cert-manager/subscribers",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers"
),
CertificatesPage: setRoute(
"/cert-manager/$projectId/certificates",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates"
"/projects/$projectId/cert-manager/certificates",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificates"
),
CertificateAuthoritiesPage: setRoute(
"/cert-manager/$projectId/certificate-authorities",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities"
"/projects/$projectId/cert-manager/certificate-authorities",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificate-authorities"
),
AlertingPage: setRoute(
"/cert-manager/$projectId/alerting",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting"
"/projects/$projectId/cert-manager/alerting",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/alerting"
),
PkiCollectionDetailsByIDPage: setRoute(
"/cert-manager/$projectId/pki-collections/$collectionId",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
"/projects/$projectId/cert-manager/pki-collections/$collectionId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/pki-collections/$collectionId"
),
PkiSubscriberDetailsByIDPage: setRoute(
"/cert-manager/$projectId/subscribers/$subscriberName",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName"
"/projects/$projectId/cert-manager/subscribers/$subscriberName",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers/$subscriberName"
)
},
Ssh: {
SshCaByIDPage: setRoute(
"/ssh/$projectId/ca/$caId",
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId"
"/projects/$projectId/ssh/ca/$caId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/ca/$caId"
),
SshHostGroupDetailsByIDPage: setRoute(
"/ssh/$projectId/ssh-host-groups/$sshHostGroupId",
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId"
"/projects/$projectId/ssh/ssh-host-groups/$sshHostGroupId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/ssh-host-groups/$sshHostGroupId"
)
},
SecretScanning: {
DataSourceByIdPage: setRoute(
"/secret-scanning/$projectId/data-sources/$type/$dataSourceId",
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/data-sources/$type/$dataSourceId"
"/projects/$projectId/secret-scanning/data-sources/$type/$dataSourceId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout/data-sources/$type/$dataSourceId"
),
FindingsPage: setRoute(
"/secret-scanning/$projectId/findings",
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/findings"
"/projects/$projectId/secret-scanning/findings",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout/findings"
)
},
Public: {
+5 -2
View File
@@ -37,7 +37,8 @@ import {
TeamCityConnectionMethod,
TerraformCloudConnectionMethod,
VercelConnectionMethod,
WindmillConnectionMethod
WindmillConnectionMethod,
ZabbixConnectionMethod
} from "@app/hooks/api/appConnections/types";
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection";
@@ -88,7 +89,8 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Render]: { name: "Render", image: "Render.png" },
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
[AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" },
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" }
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" }
};
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@@ -120,6 +122,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case VercelConnectionMethod.ApiToken:
case OnePassConnectionMethod.ApiToken:
case CloudflareConnectionMethod.ApiToken:
case ZabbixConnectionMethod.ApiToken:
return { name: "API Token", icon: faKey };
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
+12 -8
View File
@@ -1,6 +1,6 @@
import { apiRequest } from "@app/config/request";
import { createWorkspace } from "@app/hooks/api/workspace/queries";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
const secretsToBeAdded = [
{
@@ -42,8 +42,7 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
const {
data: { project }
} = await createWorkspace({
projectName,
type: ProjectType.SecretManager
projectName
});
try {
@@ -60,14 +59,14 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
return project;
};
export const getProjectHomePage = (workspace: Workspace) => {
switch (workspace.type) {
export const getProjectHomePage = (type: ProjectType) => {
switch (type) {
case ProjectType.CertificateManager:
return `/${workspace.type}/$projectId/subscribers` as const;
return `/projects/$projectId/${type}/subscribers` as const;
case ProjectType.SecretScanning:
return `/${workspace.type}/$projectId/data-sources` as const;
return `/projects/$projectId/${type}/data-sources` as const;
default:
return `/${workspace.type}/$projectId/overview` as const;
return `/projects/$projectId/${type}/overview` as const;
}
};
@@ -81,3 +80,8 @@ export const getProjectTitle = (type: ProjectType) => {
};
return titleConvert[type];
};
export const getCurrentProductFromUrl = (location: string) => {
const type = Object.values(ProjectType).find((el) => location.includes(`/${el}`));
return type;
};
+6 -1
View File
@@ -81,6 +81,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
[SecretSync.CloudflarePages]: {
name: "Cloudflare Pages",
image: "Cloudflare.png"
},
[SecretSync.Zabbix]: {
name: "Zabbix",
image: "Zabbix.png"
}
};
@@ -106,7 +110,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Render]: AppConnection.Render,
[SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.GitLab]: AppConnection.Gitlab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare
[SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.Zabbix]: AppConnection.Zabbix
};
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
@@ -25,8 +25,8 @@ export const accessApprovalKeys = {
requestedBy?: string,
bypassReason?: string
) => [{ projectSlug, envSlug, requestedBy, bypassReason }, "access-approvals-requests"] as const,
getAccessApprovalRequestCount: (projectSlug: string) =>
[{ projectSlug }, "access-approval-request-count"] as const
getAccessApprovalRequestCount: (projectSlug: string, policyId?: string) =>
[{ projectSlug }, "access-approval-request-count", ...(policyId ? [policyId] : [])] as const
};
export const fetchPolicyApprovalCount = async ({
@@ -87,21 +87,22 @@ const fetchApprovalRequests = async ({
}));
};
const fetchAccessRequestsCount = async (projectSlug: string) => {
const fetchAccessRequestsCount = async (projectSlug: string, policyId?: string) => {
const { data } = await apiRequest.get<TAccessRequestCount>(
"/api/v1/access-approvals/requests/count",
{ params: { projectSlug } }
{ params: { projectSlug, policyId } }
);
return data;
};
export const useGetAccessRequestsCount = ({
projectSlug,
policyId,
options = {}
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
useQuery({
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug),
queryFn: () => fetchAccessRequestsCount(projectSlug),
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug, policyId),
queryFn: () => fetchAccessRequestsCount(projectSlug, policyId),
...options,
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
});
@@ -147,6 +147,7 @@ export type TCreateAccessRequestDTO = {
export type TGetAccessApprovalRequestsDTO = {
projectSlug: string;
policyId?: string;
envSlug?: string;
authorUserId?: string;
};
@@ -27,5 +27,6 @@ export enum AppConnection {
Render = "render",
Flyio = "flyio",
Gitlab = "gitlab",
Cloudflare = "cloudflare"
Cloudflare = "cloudflare",
Zabbix = "zabbix"
}
@@ -132,6 +132,10 @@ export type TCloudflareConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Cloudflare;
};
export type TZabbixConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Zabbix;
};
export type TAppConnectionOption =
| TAwsConnectionOption
| TGitHubConnectionOption
@@ -159,7 +163,8 @@ export type TAppConnectionOption =
| TRenderConnectionOption
| TFlyioConnectionOption
| TGitlabConnectionOption
| TCloudflareConnectionOption;
| TCloudflareConnectionOption
| TZabbixConnectionOption;
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
@@ -191,4 +196,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.Flyio]: TFlyioConnectionOption;
[AppConnection.Gitlab]: TGitlabConnectionOption;
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
[AppConnection.Zabbix]: TZabbixConnectionOption;
};
@@ -29,6 +29,7 @@ import { TTeamCityConnection } from "./teamcity-connection";
import { TTerraformCloudConnection } from "./terraform-cloud-connection";
import { TVercelConnection } from "./vercel-connection";
import { TWindmillConnection } from "./windmill-connection";
import { TZabbixConnection } from "./zabbix-connection";
export * from "./1password-connection";
export * from "./auth0-connection";
@@ -59,6 +60,7 @@ export * from "./teamcity-connection";
export * from "./terraform-cloud-connection";
export * from "./vercel-connection";
export * from "./windmill-connection";
export * from "./zabbix-connection";
export type TAppConnection =
| TAwsConnection
@@ -89,7 +91,8 @@ export type TAppConnection =
| TRenderConnection
| TFlyioConnection
| TGitLabConnection
| TCloudflareConnection;
| TCloudflareConnection
| TZabbixConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
@@ -146,4 +149,5 @@ export type TAppConnectionMap = {
[AppConnection.Flyio]: TFlyioConnection;
[AppConnection.Gitlab]: TGitLabConnection;
[AppConnection.Cloudflare]: TCloudflareConnection;
[AppConnection.Zabbix]: TZabbixConnection;
};
@@ -0,0 +1,14 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum ZabbixConnectionMethod {
ApiToken = "api-token"
}
export type TZabbixConnection = TRootAppConnection & { app: AppConnection.Zabbix } & {
method: ZabbixConnectionMethod.ApiToken;
credentials: {
apiToken: string;
instanceUrl: string;
};
};
@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";
@@ -0,0 +1,36 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import { TZabbixHost } from "./types";
const zabbixConnectionKeys = {
all: [...appConnectionKeys.all, "zabbix"] as const,
listHosts: (connectionId: string) => [...zabbixConnectionKeys.all, "hosts", connectionId] as const
};
export const useZabbixConnectionListHosts = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TZabbixHost[],
unknown,
TZabbixHost[],
ReturnType<typeof zabbixConnectionKeys.listHosts>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: zabbixConnectionKeys.listHosts(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TZabbixHost[]>(
`/api/v1/app-connections/zabbix/${connectionId}/hosts`
);
return data;
},
...options
});
};
@@ -0,0 +1,25 @@
export type TZabbixHost = {
host: string;
hostId: string;
};
export enum ZabbixSyncScope {
Host = "host",
Global = "global"
}
export enum ZabbixMacroType {
Text = 0,
Secret = 1
}
export const ZABBIX_SYNC_SCOPES = {
[ZabbixSyncScope.Host]: {
name: "Host",
description: "Sync secrets to a specific host in Zabbix."
},
[ZabbixSyncScope.Global]: {
name: "Global",
description: "Sync secrets to a global scope in Zabbix."
}
};
+2 -2
View File
@@ -30,8 +30,8 @@ export const useGetAuditLogs = (
params: {
...filters,
offset: pageParam,
startDate: filters?.startDate?.toISOString(),
endDate: filters?.endDate?.toISOString(),
startDate: filters.startDate.toISOString(),
endDate: filters.endDate.toISOString(),
...(filters.eventMetadata && Object.keys(filters.eventMetadata).length
? {
eventMetadata: Object.entries(filters.eventMetadata)
+2 -2
View File
@@ -14,8 +14,8 @@ export type TGetAuditLogsFilter = {
actor?: string; // user ID format
secretPath?: string;
secretKey?: string;
startDate?: Date;
endDate?: Date;
startDate: Date;
endDate: Date;
limit: number;
};
+1 -1
View File
@@ -51,7 +51,7 @@ export type IdentityMembershipOrg = {
export type IdentityMembership = {
id: string;
identity: Identity;
project: Pick<Workspace, "id" | "name" | "type">;
project: Pick<Workspace, "id" | "name" | "defaultProduct">;
roles: Array<
{
id: string;
@@ -6,17 +6,14 @@ import {
TProjectTemplate,
TProjectTemplateResponse
} from "@app/hooks/api/projectTemplates/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const projectTemplateKeys = {
all: ["project-template"] as const,
list: (projectType?: ProjectType) =>
[...projectTemplateKeys.all, "list", ...(projectType ? [projectType] : [])] as const,
list: () => [...projectTemplateKeys.all, "list"] as const,
byId: (templateId: string) => [...projectTemplateKeys.all, templateId] as const
};
export const useListProjectTemplates = (
type?: ProjectType,
options?: Omit<
UseQueryOptions<
TProjectTemplate[],
@@ -28,11 +25,9 @@ export const useListProjectTemplates = (
>
) => {
return useQuery({
queryKey: projectTemplateKeys.list(type),
queryKey: projectTemplateKeys.list(),
queryFn: async () => {
const { data } = await apiRequest.get<TListProjectTemplates>("/api/v1/project-templates", {
params: { type }
});
const { data } = await apiRequest.get<TListProjectTemplates>("/api/v1/project-templates");
return data.projectTemplates;
},
@@ -1,10 +1,8 @@
import { TProjectRole } from "@app/hooks/api/roles/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
export type TProjectTemplate = {
id: string;
name: string;
type: ProjectType;
description?: string;
roles: Pick<TProjectRole, "slug" | "name" | "permissions">[];
environments?: { name: string; slug: string; position: number }[] | null;
@@ -16,7 +14,6 @@ export type TListProjectTemplates = { projectTemplates: TProjectTemplate[] };
export type TProjectTemplateResponse = { projectTemplate: TProjectTemplate };
export type TCreateProjectTemplateDTO = {
type: ProjectType;
name: string;
description?: string;
};
@@ -34,9 +34,10 @@ export const secretApprovalRequestKeys = {
] as const,
detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) =>
[{ id }, "secret-approval-request-detail"] as const,
count: ({ workspaceId }: TGetSecretApprovalRequestCount) => [
count: ({ workspaceId, policyId }: TGetSecretApprovalRequestCount) => [
{ workspaceId },
"secret-approval-request-count"
"secret-approval-request-count",
...(policyId ? [policyId] : [])
]
};
@@ -204,10 +205,13 @@ export const useGetSecretApprovalRequestDetails = ({
enabled: Boolean(id) && (options?.enabled ?? true)
});
const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => {
const fetchSecretApprovalRequestCount = async ({
workspaceId,
policyId
}: TGetSecretApprovalRequestCount) => {
const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequestCount }>(
"/api/v1/secret-approval-requests/count",
{ params: { workspaceId } }
{ params: { workspaceId, policyId } }
);
return data.approvals;
@@ -215,6 +219,7 @@ const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprov
export const useGetSecretApprovalRequestCount = ({
workspaceId,
policyId,
options = {}
}: TGetSecretApprovalRequestCount & {
options?: Omit<
@@ -228,8 +233,8 @@ export const useGetSecretApprovalRequestCount = ({
>;
}) =>
useQuery({
queryKey: secretApprovalRequestKeys.count({ workspaceId }),
queryKey: secretApprovalRequestKeys.count({ workspaceId, policyId }),
refetchInterval: 15000,
queryFn: () => fetchSecretApprovalRequestCount({ workspaceId }),
queryFn: () => fetchSecretApprovalRequestCount({ workspaceId, policyId }),
enabled: Boolean(workspaceId) && (options?.enabled ?? true)
});
@@ -118,6 +118,7 @@ export type TGetSecretApprovalRequestList = {
export type TGetSecretApprovalRequestCount = {
workspaceId: string;
policyId?: string;
};
export type TGetSecretApprovalRequestDetails = {
+2 -1
View File
@@ -20,7 +20,8 @@ export enum SecretSync {
Render = "render",
Flyio = "flyio",
GitLab = "gitlab",
CloudflarePages = "cloudflare-pages"
CloudflarePages = "cloudflare-pages",
Zabbix = "zabbix"
}
export enum SecretSyncStatus {
@@ -23,6 +23,7 @@ import { TTeamCitySync } from "./teamcity-sync";
import { TTerraformCloudSync } from "./terraform-cloud-sync";
import { TVercelSync } from "./vercel-sync";
import { TWindmillSync } from "./windmill-sync";
import { TZabbixSync } from "./zabbix-sync";
export type TSecretSyncOption = {
name: string;
@@ -53,7 +54,8 @@ export type TSecretSync =
| TRenderSync
| TFlyioSync
| TGitLabSync
| TCloudflarePagesSync;
| TCloudflarePagesSync
| TZabbixSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
@@ -0,0 +1,25 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
import { ZabbixSyncScope } from "../../appConnections/zabbix";
export type TZabbixSync = TRootSecretSync & {
destination: SecretSync.Zabbix;
destinationConfig:
| {
scope: ZabbixSyncScope.Host;
hostId: string;
hostName: string;
macroType: number;
}
| {
scope: ZabbixSyncScope.Global;
macroType: number;
};
connection: {
app: AppConnection.Zabbix;
name: string;
id: string;
};
};
+15 -16
View File
@@ -154,16 +154,14 @@ export const useGetWorkspaceById = (
export const useGetUserWorkspaces = ({
includeRoles,
type = "all",
options = {}
}: {
includeRoles?: boolean;
type?: ProjectType | "all";
options?: { enabled?: boolean };
} = {}) =>
useQuery({
queryKey: workspaceKeys.getAllUserWorkspace(type),
queryFn: () => fetchUserWorkspaces(includeRoles, type),
queryKey: workspaceKeys.getAllUserWorkspace(),
queryFn: () => fetchUserWorkspaces(includeRoles),
...options
});
@@ -257,17 +255,16 @@ export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { project: Workspace } }, object, CreateWorkspaceDTO>({
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template, type }) =>
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) =>
createWorkspace({
projectName,
projectDescription,
kmsKeyId,
template,
type
template
}),
onSuccess: (dto) => {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getAllUserWorkspace(dto.data.project.type)
queryKey: workspaceKeys.getAllUserWorkspace()
});
}
});
@@ -283,7 +280,8 @@ export const useUpdateProject = () => {
newProjectDescription,
newSlug,
secretSharing,
showSnapshotsLegacy
showSnapshotsLegacy,
defaultProduct
}) => {
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
`/api/v1/workspace/${projectID}`,
@@ -291,6 +289,7 @@ export const useUpdateProject = () => {
name: newProjectName,
description: newProjectDescription,
slug: newSlug,
defaultProduct,
secretSharing,
showSnapshotsLegacy
}
@@ -351,8 +350,8 @@ export const useUpdateWorkspaceVersionLimit = () => {
});
return data.workspace;
},
onSuccess: (dto) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace(dto.type) });
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
}
});
};
@@ -370,8 +369,8 @@ export const useUpdateWorkspaceAuditLogsRetention = () => {
);
return data.workspace;
},
onSuccess: (dto) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace(dto.type) });
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
}
});
};
@@ -384,8 +383,8 @@ export const useDeleteWorkspace = () => {
const { data } = await apiRequest.delete(`/api/v1/workspace/${workspaceID}`);
return data.workspace;
},
onSuccess: (dto) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace(dto.type) });
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
queryClient.invalidateQueries({
queryKey: ["org-admin-projects"]
});
@@ -12,8 +12,7 @@ export const workspaceKeys = {
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
getAllUserWorkspace: (type?: string) =>
type ? ["workspaces", { type }] : (["workspaces"] as const),
getAllUserWorkspace: () => ["workspaces"] as const,
getWorkspaceAuditLogs: (workspaceId: string) =>
[{ workspaceId }, "workspace-audit-logs"] as const,
getWorkspaceUsers: (
+2 -3
View File
@@ -24,7 +24,7 @@ export type Workspace = {
__v: number;
id: string;
name: string;
type: ProjectType;
defaultProduct: ProjectType;
description?: string;
orgId: string;
version: ProjectVersion;
@@ -71,7 +71,6 @@ export type CreateWorkspaceDTO = {
projectDescription?: string;
kmsKeyId?: string;
template?: string;
type: ProjectType;
};
export type UpdateProjectDTO = {
@@ -81,6 +80,7 @@ export type UpdateProjectDTO = {
newSlug?: string;
secretSharing?: boolean;
showSnapshotsLegacy?: boolean;
defaultProduct?: ProjectType;
};
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
@@ -181,7 +181,6 @@ export enum ProjectIdentityOrderBy {
Name = "name"
}
export type TSearchProjectsDTO = {
type?: ProjectType;
name?: string;
limit?: number;
offset?: number;
+2
View File
@@ -1,9 +1,11 @@
export { useDebounce } from "./useDebounce";
export * from "./useGetProjectTypeFromRoute";
export { useLocalStorageState } from "./useLocalStorageState";
export { usePagination } from "./usePagination";
export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp";
export { useResetPageHelper } from "./useResetPageHelper";
export * from "./useResizableHeaderHeight";
export { useSyntaxHighlight } from "./useSyntaxHighlight";
export { useTimedReset } from "./useTimedReset";
export { useToggle } from "./useToggle";
@@ -8,15 +8,10 @@ export const useGetProjectTypeFromRoute = () => {
return useMemo(() => {
const segments = location.pathname.split("/");
const type = segments?.[2];
if (!type) return ProjectType.SecretManager;
let type: ProjectType | undefined;
// location of project type can vary in router path, so we need to check all possible values
segments.forEach((segment) => {
if (Object.values(ProjectType).includes(segment as ProjectType))
type = segment as ProjectType;
});
return type;
// second element would be /projects/$projectId/<type>
return Object.values(ProjectType).find((el) => el === type) || ProjectType.SecretManager;
}, [location]);
};
@@ -0,0 +1,70 @@
import { useCallback, useEffect, useSyncExternalStore } from "react";
type SetStateAction<T> = T | ((prevState: T) => T);
const dispatchStorageEvent = (key: string, newValue: string | null): void => {
window.dispatchEvent(new StorageEvent("storage", { key, newValue }));
};
const setLocalStorageItem = (key: string, value: unknown): void => {
const stringifiedValue = JSON.stringify(value);
window.localStorage.setItem(key, stringifiedValue);
dispatchStorageEvent(key, stringifiedValue);
};
const removeLocalStorageItem = (key: string): void => {
window.localStorage.removeItem(key);
dispatchStorageEvent(key, null);
};
const getLocalStorageItem = (key: string): string | null => {
return window.localStorage.getItem(key);
};
const useLocalStorageSubscribe = (callback: (e: StorageEvent) => void) => {
window.addEventListener("storage", callback);
return () => window.removeEventListener("storage", callback);
};
const getLocalStorageServerSnapshot = (): never => {
throw Error("useLocalStorage is a client-only hook");
};
export const useLocalStorageState = <T>(
key: string,
initialValue: T
): [T, (value: SetStateAction<T>) => void] => {
const getSnapshot = () => getLocalStorageItem(key);
const store = useSyncExternalStore(
useLocalStorageSubscribe,
getSnapshot,
getLocalStorageServerSnapshot
);
const setState = useCallback(
(v: SetStateAction<T>): void => {
try {
const nextState =
typeof v === "function" ? (v as (prevState: T) => T)(JSON.parse(store || "null")) : v;
if (nextState === undefined || nextState === null) {
removeLocalStorageItem(key);
} else {
setLocalStorageItem(key, nextState);
}
} catch (e) {
console.warn(e);
}
},
[key, store]
);
useEffect(() => {
if (getLocalStorageItem(key) === null && typeof initialValue !== "undefined") {
setLocalStorageItem(key, initialValue);
}
}, [key, initialValue]);
return [store ? JSON.parse(store) : initialValue, setState];
};
@@ -0,0 +1,71 @@
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react";
type Params = {
minHeight: number;
maxHeight: number;
initialHeight: number;
};
export const useResizableHeaderHeight = ({ minHeight, maxHeight, initialHeight }: Params) => {
const [headerHeight, setHeaderHeight] = useState(initialHeight);
const [isResizing, setIsResizing] = useState(false);
const startY = useRef(0);
const startHeight = useRef(0);
const handleMouseDown = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsResizing(true);
startY.current = e.clientY;
startHeight.current = headerHeight;
},
[headerHeight]
);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isResizing) return;
const deltaY = e.clientY - startY.current;
const newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight.current + deltaY));
setHeaderHeight(newHeight);
},
[isResizing]
);
const handleMouseUp = useCallback(() => {
setIsResizing(false);
}, []);
useEffect(() => {
if (isResizing) {
document.addEventListener(
"mousemove",
// @ts-expect-error native discrepancy
handleMouseMove
);
document.addEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "ns-resize";
document.body.style.userSelect = "none";
}
return () => {
document.removeEventListener(
"mousemove",
// @ts-expect-error native discrepancy
handleMouseMove
);
document.removeEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
}, [isResizing, handleMouseMove, handleMouseUp]);
return {
headerHeight,
handleMouseDown,
isResizing
};
};
+20 -59
View File
@@ -1,20 +1,8 @@
import { faArrowLeft, faInfo, faQuestion } from "@fortawesome/free-solid-svg-icons";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useMatchRoute } from "@tanstack/react-router";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Menu,
MenuGroup,
MenuItem
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
const generalTabs = [
{
@@ -74,21 +62,13 @@ export const AdminSidebar = () => {
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div className="flex-grow">
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}>
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
Back to organization
</div>
</Link>
<Menu>
<MenuGroup title="General">
{generalTabs.map((tab) => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<MenuItem isSelected={Boolean(isActive)} icon={tab.icon}>
{tab.label}
</MenuItem>
<MenuItem isSelected={Boolean(isActive)}>{tab.label}</MenuItem>
</Link>
);
})}
@@ -98,48 +78,29 @@ export const AdminSidebar = () => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<MenuItem isSelected={Boolean(isActive)} icon={tab.icon}>
{tab.label}
</MenuItem>
<MenuItem isSelected={Boolean(isActive)}>{tab.label}</MenuItem>
</Link>
);
})}
</MenuGroup>
</Menu>
</div>
<div className="relative mb-4 mt-10 flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
Help & Support
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
<DropdownMenuItem key={url as string}>
<a
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
</DropdownMenuItem>
))}
{envConfig.PLATFORM_VERSION && (
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Version: {envConfig.PLATFORM_VERSION}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
<Menu>
<Link to="/organization/projects">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faChevronLeft}
flip="vertical"
/>
}
>
Back to organization
</MenuItem>
</Link>
</Menu>
</nav>
</aside>
);
@@ -0,0 +1,61 @@
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
export const KmsLayout = () => {
const { currentWorkspace } = useWorkspace();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">KMS</div>
<div className="flex-1">
<Menu>
<Link
to="/projects/$projectId/kms/overview"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Overview</MenuItem>}
</Link>
<Link
to="/projects/$projectId/kms/kmip"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>KMIP</MenuItem>}
</Link>
{
// <Link
// to="/projects/$projectId/kms/settings"
// params={{
// projectId: currentWorkspace.id
// }}
// >
// {({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
// </Link>
}
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
<Outlet />
</div>
</div>
</div>
);
};
+1
View File
@@ -0,0 +1 @@
export { KmsLayout } from "./KmsLayout";
@@ -1,121 +1,48 @@
import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { linkOptions, Outlet, useLocation, useRouterState } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import { Outlet, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { Banner } from "@app/components/page-frames/Banner";
import { BreadcrumbContainer, TBreadcrumbFormat } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission, useServerConfig } from "@app/context";
import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types";
import { useServerConfig } from "@app/context";
import { usePopUp } from "@app/hooks";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
import { SidebarHeader } from "./components/SidebarHeader";
import { DefaultSideBar, ProjectOverviewSideBar, SecretSharingSideBar } from "./ProductsSideBar";
import { Navbar } from "./components/NavBar";
import { OrgSidebar } from "./components/OrgSidebar";
export const OrganizationLayout = () => {
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const location = useLocation();
const { config } = useServerConfig();
const { permission } = useOrgPermission();
const shouldShowProductsSidebar = permission.can(
OrgPermissionSecretShareAction.ManageSettings,
OrgPermissionSubjects.SecretShare
);
const isOrganizationSpecificPage = location.pathname.startsWith("/organization");
const breadcrumbs =
isOrganizationSpecificPage && matches && "breadcrumbs" in matches
? matches.breadcrumbs
: undefined;
const projectId = useParams({
strict: false,
select: (el) => el?.projectId
});
const isInsideProject = Boolean(projectId);
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { t } = useTranslation();
const isSecretSharingPage = (
[
linkOptions({ to: "/organization/secret-sharing" }).to,
linkOptions({ to: "/organization/secret-sharing/settings" }).to
] as string[]
).includes(location.pathname);
const isProjectOverviewOrSettingsPage = (
[
linkOptions({ to: "/organization/secret-manager/overview" }).to,
linkOptions({ to: "/organization/secret-manager/settings" }).to,
linkOptions({ to: "/organization/cert-manager/overview" }).to,
linkOptions({ to: "/organization/cert-manager/settings" }).to,
linkOptions({ to: "/organization/kms/overview" }).to,
linkOptions({ to: "/organization/kms/settings" }).to,
linkOptions({ to: "/organization/ssh/overview" }).to,
linkOptions({ to: "/organization/ssh/settings" }).to,
linkOptions({ to: "/organization/secret-scanning/overview" }).to,
linkOptions({ to: "/organization/secret-scanning/settings" }).to
] as string[]
).includes(location.pathname);
const shouldShowOrgSidebar =
location.pathname.startsWith("/organization") &&
(!isSecretSharingPage || shouldShowProductsSidebar);
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
let SideBarComponent = <DefaultSideBar />;
if (isSecretSharingPage) {
SideBarComponent = <SecretSharingSideBar />;
} else if (isProjectOverviewOrSettingsPage) {
SideBarComponent = (
<ProjectOverviewSideBar type={location.pathname.split("/")[2] as ProjectType} />
);
}
return (
<>
<Banner />
<div
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
>
<Navbar />
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<MinimizedOrgSidebar />
<AnimatePresence mode="popLayout">
{shouldShowOrgSidebar && (
<motion.div
key="menu-list-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-60 overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
{!isProjectOverviewOrSettingsPage && (
<div className="p-2 pt-3">
<SidebarHeader />
</div>
)}
{SideBarComponent}
</nav>
</motion.div>
)}
</AnimatePresence>
<OrgSidebar isHidden={isInsideProject} />
<main
className={twMerge(
"flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]",
!isOrganizationSpecificPage && "overflow-hidden p-0"
"flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 pt-8 dark:[color-scheme:dark]",
isInsideProject && "p-0"
)}
>
{breadcrumbs ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
) : null}
<Outlet />
</main>
</div>
@@ -1,61 +0,0 @@
import { Link } from "@tanstack/react-router";
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
export const DefaultSideBar = () => (
<Menu>
<MenuGroup title="Organization Control">
<Link to="/organization/access-management">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
)}
</Link>
<Link to="/organization/app-connections">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
App Connections
</MenuItem>
)}
</Link>
<Link to="/organization/gateways">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="gateway" iconMode="reverse">
Gateways
</MenuItem>
)}
</Link>
<Link to="/organization/sso">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="check">
Single Sign-On (SSO)
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Other">
<Link to="/organization/audit-logs">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="moving-block">
Audit Logs
</MenuItem>
)}
</Link>
<Link to="/organization/billing">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="spinning-coin">
Usage & Billing
</MenuItem>
)}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
Organization Settings
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
);
@@ -1,99 +0,0 @@
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useMatchRoute } from "@tanstack/react-router";
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { ProjectType } from "@app/hooks/api/workspace/types";
type TProjectOverviewSideBarProps = {
type: ProjectType;
};
export const ProjectOverviewSideBar = ({ type }: TProjectOverviewSideBarProps) => {
const matchRoute = useMatchRoute();
const isOverviewActive = !!matchRoute({
to: `/organization/${type}/overview`,
fuzzy: false
});
let label: string;
let icon: string;
let link: string;
switch (type) {
case ProjectType.CertificateManager:
label = "Cert Management";
icon = "note";
link = "https://infisical.com/docs/documentation/platform/pki/overview";
break;
case ProjectType.SecretManager:
label = "Secret Management";
icon = "sliding-carousel";
link = "https://infisical.com/docs/documentation/getting-started/introduction";
break;
case ProjectType.KMS:
label = "KMS";
icon = "unlock";
link = "https://infisical.com/docs/documentation/platform/kms/overview";
break;
case ProjectType.SSH:
label = "SSH";
icon = "verified";
link = "https://infisical.com/docs/documentation/platform/ssh/overview";
break;
case ProjectType.SecretScanning:
label = "Secret Scanning";
icon = "secret-scan";
link = "https://infisical.com/docs/documentation/platform/secret-scanning/overview";
break;
default:
throw new Error("Unknown project type");
}
return (
<>
<div className="p-2 pt-3">
<a
target="_blank"
rel="noopener noreferrer"
href={link}
className="flex w-full items-center rounded-md border border-mineshaft-600 p-2 pl-3 transition-all duration-150 hover:bg-mineshaft-700"
>
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-mineshaft-500 p-4">
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="h-3.5 w-3.5 text-mineshaft-300"
/>
</div>
<div className="-mt-1 flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
Infisical Docs
</div>
<div className="text-xs leading-[10px] text-mineshaft-400">
{type === ProjectType.SecretManager ? "Get Started" : `${label} Overview`}
</div>
</div>
</a>
</div>
<Menu>
<MenuGroup title="Overview">
<Link to={`/organization/${type}/overview`}>
<MenuItem isSelected={isOverviewActive} icon={icon}>
{label}
</MenuItem>
</Link>
</MenuGroup>
<MenuGroup title="Other">
<Link to={`/organization/${type}/settings`}>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
Settings
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</>
);
};
@@ -1,34 +0,0 @@
import { Link, useMatchRoute } from "@tanstack/react-router";
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
export const SecretSharingSideBar = () => {
const matchRoute = useMatchRoute();
const isOverviewActive = !!matchRoute({
to: "/organization/secret-sharing",
fuzzy: false
});
return (
<Menu>
<MenuGroup title="Overview">
<Link to="/organization/secret-sharing">
{() => (
<MenuItem isSelected={isOverviewActive} icon="lock-closed">
Secret Sharing
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Other">
<Link to="/organization/secret-sharing/settings">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
Settings
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
);
};
@@ -1,3 +0,0 @@
export * from "./DefaultSideBar";
export * from "./ProjectOverviewSideBar";
export * from "./SecretSharingSideBar";
@@ -1,644 +0,0 @@
import { useState } from "react";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import {
faArrowUpRightFromSquare,
faBook,
faCheck,
faCheckCircle,
faCog,
faDoorClosed,
faEnvelope,
faInfinity,
faInfo,
faInfoCircle,
faMoneyBill,
faPlug,
faSignOut,
faUser,
faUserCog,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { Link, linkOptions, useLocation, useNavigate, useRouter } from "@tanstack/react-router";
import { Mfa } from "@app/components/auth/Mfa";
import { createNotification } from "@app/components/notifications";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import SecurityClient from "@app/components/utilities/SecurityClient";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
Modal,
ModalContent,
Tooltip
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
import { usePopUp, useToggle } from "@app/hooks";
import {
useGetOrganizations,
useGetOrgTrialUrl,
useLogoutUser,
useSelectOrganization,
workspaceKeys
} from "@app/hooks/api";
import { authKeys } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery";
import { SubscriptionPlan } from "@app/hooks/api/types";
import { AuthMethod } from "@app/hooks/api/users/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { MenuIconButton } from "../MenuIconButton";
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
const getPlan = (subscription: SubscriptionPlan) => {
if (subscription.groups) return "Enterprise Plan";
if (subscription.pitRecovery) return "Pro Plan";
return "Free Plan";
};
export const INFISICAL_SUPPORT_OPTIONS = [
[
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
"Support Forum",
"https://infisical.com/slack"
],
[
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
"Read Docs",
"https://infisical.com/docs/documentation/getting-started/introduction"
],
[
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
"GitHub Issues",
"https://github.com/Infisical/infisical/issues"
],
[
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
"Email Support",
"mailto:[email protected]"
],
[
<FontAwesomeIcon key={5} className="pr-4 text-sm" icon={faUsers} />,
"Instance Admins",
"server-admins"
]
];
export const MinimizedOrgSidebar = () => {
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { subscription } = useSubscription();
const [open, setOpen] = useState(false);
const [openSupport, setOpenSupport] = useState(false);
const [openUser, setOpenUser] = useState(false);
const [openOrg, setOpenOrg] = useState(false);
const [showAdminsModal, setShowAdminsModal] = useState(false);
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
const { currentOrg } = useOrganization();
const { data: orgs } = useGetOrganizations();
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { mutateAsync: selectOrganization } = useSelectOrganization();
const navigate = useNavigate();
const router = useRouter();
const location = useLocation();
const queryClient = useQueryClient();
const isMoreSelected = (
[
linkOptions({ to: "/organization/access-management" }).to,
linkOptions({ to: "/organization/app-connections" }).to,
linkOptions({ to: "/organization/billing" }).to,
linkOptions({ to: "/organization/sso" }).to,
linkOptions({ to: "/organization/gateways" }).to,
linkOptions({ to: "/organization/settings" }).to,
linkOptions({ to: "/organization/audit-logs" }).to
] as string[]
).includes(location.pathname);
const handleOrgChange = async (orgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
return;
}
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
};
const logout = useLogoutUser();
const logOutUser = async () => {
try {
console.log("Logging out...");
await logout.mutateAsync();
navigate({ to: "/login" });
} catch (error) {
console.error(error);
}
};
const handleCopyToken = async () => {
try {
await window.navigator.clipboard.writeText(getAuthToken());
createNotification({
type: "success",
text: "Copied current login session token to clipboard"
});
} catch (error) {
console.log(error);
createNotification({ type: "error", text: "Failed to copy user token to clipboard" });
}
};
if (shouldShowMfa) {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Mfa
email={user.email as string}
method={requiredMfaMethod}
successCallback={mfaSuccessCallback}
closeMfa={() => toggleShowMfa.off()}
/>
</div>
);
}
return (
<>
<aside
className="dark z-10 border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 transition-all duration-150"
style={{ width: "72px" }}
>
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div>
<div className="flex items-center hover:bg-mineshaft-700">
<DropdownMenu open={openOrg} onOpenChange={setOpenOrg} modal={false}>
<DropdownMenuTrigger
onMouseEnter={() => setOpenOrg(true)}
onMouseLeave={() => setOpenOrg(false)}
asChild
>
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-3 pb-5 pt-6 transition-all">
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
{currentOrg?.name.charAt(0)}
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
onMouseEnter={() => setOpenOrg(true)}
onMouseLeave={() => setOpenOrg(false)}
align="start"
side="right"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-0.5 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/5 to-mineshaft-800 p-1 transition-all duration-300">
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary text-black">
{currentOrg?.name.charAt(0)}
</div>
<div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
{currentOrg?.name}
</div>
<div className="text-xs text-mineshaft-400">{getPlan(subscription)}</div>
</div>
</div>
</div>
<div className="px-2 py-1 text-xs capitalize text-mineshaft-400">
organizations
</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
</div>
</Button>
</DropdownMenuItem>
);
})}
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faSignOut} />}
onClick={logOutUser}
>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="space-y-1">
{currentOrg.secretsProductEnabled && (
<Link to="/organization/secret-manager/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(
`/organization/${ProjectType.SecretManager}`
)
}
icon="sliding-carousel"
>
Secrets
</MenuIconButton>
)}
</Link>
)}
{currentOrg.pkiProductEnabled && (
<Link to="/organization/cert-manager/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(
`/organization/${ProjectType.CertificateManager}`
)
}
icon="note"
>
PKI
</MenuIconButton>
)}
</Link>
)}
{currentOrg.kmsProductEnabled && (
<Link to="/organization/kms/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/organization/${ProjectType.KMS}`)
}
icon="unlock"
>
KMS
</MenuIconButton>
)}
</Link>
)}
{currentOrg.sshProductEnabled && (
<Link to="/organization/ssh/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/organization/${ProjectType.SSH}`)
}
icon="verified"
>
SSH
</MenuIconButton>
)}
</Link>
)}
{currentOrg.scannerProductEnabled && (
<Link to="/organization/secret-scanning/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(
`/organization/${ProjectType.SecretScanning}`
)
}
icon="secret-scan"
>
Scanner
</MenuIconButton>
)}
</Link>
)}
{(currentOrg.scannerProductEnabled || currentOrg.shareSecretsProductEnabled) && (
<div className="w-full bg-mineshaft-500" style={{ height: "1px" }} />
)}
{currentOrg.shareSecretsProductEnabled && (
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="lock-closed">
Share
</MenuIconButton>
)}
</Link>
)}
<div className="my-1 w-full bg-mineshaft-500" style={{ height: "1px" }} />
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
asChild
>
<div className="w-full">
<MenuIconButton
lottieIconMode="reverse"
icon="settings-cog"
isSelected={isMoreSelected}
>
Admin
</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
align="start"
side="right"
className="p-1"
>
<DropdownMenuLabel>Organization Options</DropdownMenuLabel>
<Link to="/organization/access-management">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faUsers} />}>
Access Control
</DropdownMenuItem>
</Link>
<Link to="/organization/app-connections">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faPlug} />}>
App Connections
</DropdownMenuItem>
</Link>
<Link to="/organization/gateways">
<DropdownMenuItem
icon={<FontAwesomeIcon className="w-3" icon={faDoorClosed} />}
>
Gateways
</DropdownMenuItem>
</Link>
<Link to="/organization/billing">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faMoneyBill} />}>
Usage & Billing
</DropdownMenuItem>
</Link>
<Link to="/organization/audit-logs">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faBook} />}>
Audit Logs
</DropdownMenuItem>
</Link>
<Link to="/organization/sso">
<DropdownMenuItem
icon={<FontAwesomeIcon className="w-3" icon={faCheckCircle} />}
>
SSO Settings
</DropdownMenuItem>
</Link>
<Link to="/organization/settings">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faCog} />}>
Organization Settings
</DropdownMenuItem>
</Link>
<DropdownMenuLabel>Admin Panels</DropdownMenuLabel>
{user?.superAdmin && (
<Link to="/admin">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faUserCog} />}>
Server Admin Console
</DropdownMenuItem>
</Link>
)}
<Link to="/organization/admin">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faCog} />}>
Organization Admin Console
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div
className={`relative mt-10 ${
subscription && subscription.slug === "starter" && !subscription.has_used_trial
? "mb-2"
: "mb-4"
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
>
<DropdownMenu open={openSupport} onOpenChange={setOpenSupport} modal={false}>
<DropdownMenuTrigger
onMouseEnter={() => setOpenSupport(true)}
onMouseLeave={() => setOpenSupport(false)}
className="w-full"
>
<MenuIconButton>
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" />
Support
</MenuIconButton>
</DropdownMenuTrigger>
<DropdownMenuContent
onMouseEnter={() => setOpenSupport(true)}
onMouseLeave={() => setOpenSupport(false)}
align="end"
side="right"
className="p-1"
>
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => {
if (url === "server-admins" && isInfisicalCloud()) {
return null;
}
return (
<DropdownMenuItem key={url as string}>
{url === "server-admins" ? (
<button
type="button"
onClick={() => setShowAdminsModal(true)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</button>
) : (
<a
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
)}
</DropdownMenuItem>
);
})}
{envConfig.PLATFORM_VERSION && (
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Version: {envConfig.PLATFORM_VERSION}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
<Tooltip content="Start Free Pro Trial" side="right">
<button
type="button"
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
success_url: window.location.href
});
window.location.href = url;
}}
className="mt-1.5 w-full"
>
<div className="justify-left mb-1.5 mt-1.5 flex w-full flex-col items-center rounded-md p-1 text-xs text-mineshaft-300 transition-all duration-150 hover:bg-mineshaft-500 hover:text-primary-400">
<FontAwesomeIcon icon={faInfinity} className="py-2 text-lg text-primary" />
Pro Trial
</div>
</button>
</Tooltip>
)}
<DropdownMenu open={openUser} onOpenChange={setOpenUser} modal={false}>
<DropdownMenuTrigger
onMouseEnter={() => setOpenUser(true)}
onMouseLeave={() => setOpenUser(false)}
className="w-full"
asChild
>
<div>
<MenuIconButton icon="user">User</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
onMouseEnter={() => setOpenUser(true)}
onMouseLeave={() => setOpenUser(false)}
side="right"
align="end"
className="p-1"
>
<div className="cursor-default px-1 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/10 to-mineshaft-800 p-1 px-2 transition-all duration-150">
<div className="p-1 pr-3">
<FontAwesomeIcon icon={faUser} className="text-xl text-mineshaft-400" />
</div>
<div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
{user?.firstName} {user?.lastName}
</div>
<div className="text-xs text-mineshaft-300">{user.email}</div>
</div>
</div>
</div>
<Link to="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link>
<a
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Documentation
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
<a
href="https://infisical.com/slack"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Join Slack Community
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem onClick={handleCopyToken}>
Copy Token
<Tooltip
content="This token is linked to your current login session and can only access resources within the organization you're currently logged into."
className="max-w-3xl"
>
<FontAwesomeIcon icon={faInfoCircle} className="mb-[0.06rem] pl-1.5 text-xs" />
</Tooltip>
</DropdownMenuItem>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</nav>
</aside>
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
<ModalContent title="Server Administrators" subTitle="View all server administrators">
<div className="mb-2">
<ServerAdminsPanel />
</div>
</ModalContent>
</Modal>
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
</>
);
};
@@ -1 +0,0 @@
export { MinimizedOrgSidebar } from "./MinimizedOrgSidebar";
@@ -0,0 +1,378 @@
import { useState } from "react";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import { faCircleQuestion, faUserCircle } from "@fortawesome/free-regular-svg-icons";
import {
faArrowUpRightFromSquare,
faBook,
faCheck,
faCubes,
faEnvelope,
faInfo,
faInfoCircle,
faSignOut,
faSort,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { Link, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
import { Mfa } from "@app/components/auth/Mfa";
import { createNotification } from "@app/components/notifications";
import SecurityClient from "@app/components/utilities/SecurityClient";
import {
BreadcrumbContainer,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Modal,
ModalContent,
TBreadcrumbFormat,
Tooltip
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useToggle } from "@app/hooks";
import { useGetOrganizations, useLogoutUser, workspaceKeys } from "@app/hooks/api";
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery";
import { SubscriptionPlan } from "@app/hooks/api/types";
import { AuthMethod } from "@app/hooks/api/users/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
const getPlan = (subscription: SubscriptionPlan) => {
if (subscription.groups) return "Enterprise";
if (subscription.pitRecovery) return "Pro";
return "Free";
};
export const INFISICAL_SUPPORT_OPTIONS = [
[
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
"Support Forum",
"https://infisical.com/slack"
],
[
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
"Read Docs",
"https://infisical.com/docs/documentation/getting-started/introduction"
],
[
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
"GitHub Issues",
"https://github.com/Infisical/infisical/issues"
],
[
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
"Email Support",
"mailto:[email protected]"
],
[
<FontAwesomeIcon key={5} className="pr-4 text-sm" icon={faUsers} />,
"Instance Admins",
"server-admins"
]
];
export const Navbar = () => {
const { user } = useUser();
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const [showAdminsModal, setShowAdminsModal] = useState(false);
const { data: orgs } = useGetOrganizations();
const navigate = useNavigate();
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const router = useRouter();
const queryClient = useQueryClient();
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
const handleOrgChange = async (orgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
return;
}
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
};
const logout = useLogoutUser();
const logOutUser = async () => {
try {
console.log("Logging out...");
await logout.mutateAsync();
navigate({ to: "/login" });
} catch (error) {
console.error(error);
}
};
const handleCopyToken = async () => {
try {
await window.navigator.clipboard.writeText(getAuthToken());
createNotification({
type: "success",
text: "Copied current login session token to clipboard"
});
} catch (error) {
console.log(error);
createNotification({ type: "error", text: "Failed to copy user token to clipboard" });
}
};
if (shouldShowMfa) {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Mfa
email={user.email as string}
method={requiredMfaMethod}
successCallback={mfaSuccessCallback}
closeMfa={() => toggleShowMfa.off()}
/>
</div>
);
}
return (
<div className="z-10 flex h-12 items-center border-b border-mineshaft-600 bg-mineshaft-800 px-4 py-2">
<div>
<Link to="/organization/projects">
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
</Link>
</div>
<div className="flex items-center">
<p className="pl-2 pr-3 text-lg text-mineshaft-400/70">/</p>
<DropdownMenu modal={false}>
<Link to="/organization/projects">
<div className="flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary-400">
<div>
<FontAwesomeIcon icon={faCubes} className="pr-1 text-xs" />
</div>
<div className="max-w-32 overflow-hidden text-ellipsis">{currentOrg?.name}</div>
<div className="mr-2 rounded border border-mineshaft-500 px-1 text-xs text-bunker-300">
{getPlan(subscription)}
</div>
</div>
</Link>
<DropdownMenuTrigger asChild>
<div>
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="switch-org"
className="rounded-md px-2 py-2 hover:bg-mineshaft-600"
>
<FontAwesomeIcon icon={faSort} className="text-xs text-bunker-300" />
</IconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs capitalize text-mineshaft-400">organizations</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
</div>
</Button>
</DropdownMenuItem>
);
})}
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem icon={<FontAwesomeIcon icon={faSignOut} />} onClick={logOutUser}>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<p className="px-2 text-lg text-mineshaft-400/70">/</p>
</div>
<div className="pl-2">
{breadcrumbs ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
) : null}
</div>
<div className="flex-grow" />
<DropdownMenu modal={false}>
<DropdownMenuTrigger>
<div className="rounded-l-md border border-r-0 border-mineshaft-500 px-2 py-1 hover:bg-mineshaft-600">
<FontAwesomeIcon icon={faCircleQuestion} className="text-mineshaft-200" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom" className="mt-3 p-1">
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => {
if (url === "server-admins" && isInfisicalCloud()) {
return null;
}
return (
<DropdownMenuItem key={url as string}>
{url === "server-admins" ? (
<button
type="button"
onClick={() => setShowAdminsModal(true)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</button>
) : (
<a
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
)}
</DropdownMenuItem>
);
})}
{envConfig.PLATFORM_VERSION && (
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Version: {envConfig.PLATFORM_VERSION}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<div className="rounded-r-md border border-mineshaft-500 px-2 py-1 hover:bg-mineshaft-600">
<FontAwesomeIcon icon={faUserCircle} className="text-mineshaft-200" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="mt-3 p-1">
<div className="cursor-default px-1 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/10 to-mineshaft-800 p-1 px-2 transition-all duration-150">
<div className="p-1 pr-3">
<FontAwesomeIcon icon={faUser} className="text-xl text-mineshaft-400" />
</div>
<div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
{user?.firstName} {user?.lastName}
</div>
<div className="text-xs text-mineshaft-300">{user.email}</div>
</div>
</div>
</div>
<Link to="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link>
<a
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Documentation
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
<a
href="https://infisical.com/slack"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Join Slack Community
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem onClick={handleCopyToken}>
Copy Token
<Tooltip
content="This token is linked to your current login session and can only access resources within the organization you're currently logged into."
className="max-w-3xl"
>
<FontAwesomeIcon icon={faInfoCircle} className="pl-1.5 text-xs" />
</Tooltip>
</DropdownMenuItem>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
<ModalContent title="Server Administrators" subTitle="View all server administrators">
<div className="mb-2">
<ServerAdminsPanel />
</div>
</ModalContent>
</Modal>
</div>
);
};
@@ -0,0 +1 @@
export { Navbar } from "./Navbar";
@@ -0,0 +1,237 @@
import {
faBook,
faCheckCircle,
faCog,
faCubes,
faDoorClosed,
faInfinity,
faMoneyBill,
faPlug,
faShare,
faUserCog,
faUsers,
faUserTie
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { Menu, MenuGroup, MenuItem, Tooltip } from "@app/components/v2";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useGetOrgTrialUrl } from "@app/hooks/api";
type Props = {
isHidden?: boolean;
};
export const OrgSidebar = ({ isHidden }: Props) => {
const { subscription } = useSubscription();
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
const { currentOrg } = useOrganization();
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
return (
<>
<AnimatePresence mode="popLayout">
{!isHidden && (
<motion.aside
key="org-sidebar"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateX: -240 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -240 }}
layout
className="dark z-10 w-60 border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-800 to-mineshaft-900"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<Menu>
<MenuGroup title="Overview">
<Link to="/organization/projects">
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCubes} />
</div>
Projects
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/access-management">
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Access Control
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/billing">
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faMoneyBill} className="mr-4" />
</div>
Usage & Billing
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/audit-logs">
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} className="mr-4" />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/sso">
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCheckCircle} className="mr-4" />
</div>
SSO Settings
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} className="mr-4" />
</div>
Organization Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Resources">
<Link to="/organization/app-connections">
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} className="mr-4" />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/gateways">
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faDoorClosed} className="mr-4" />
</div>
Gateways
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
<div className="flex-grow" />
<Menu>
{subscription &&
subscription.slug === "starter" &&
!subscription.has_used_trial && (
<Tooltip content="Start Free Pro Trial">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faInfinity}
/>
}
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
success_url: window.location.href
});
window.location.href = url;
}}
>
Pro Trial
</MenuItem>
</Tooltip>
)}
<Link to="/organization/secret-sharing">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faShare} />
</div>
}
>
Share Secret
</MenuItem>
</Link>
<Link to="/organization/admin">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faUserCog} />
</div>
}
>
Organization Admin
</MenuItem>
</Link>
{user.superAdmin && (
<Link to="/admin">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faUserTie}
/>
</div>
}
>
Server Console
</MenuItem>
</Link>
)}
</Menu>
</nav>
</motion.aside>
)}
</AnimatePresence>
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
</>
);
};
@@ -0,0 +1 @@
export { OrgSidebar } from "./OrgSidebar";
@@ -11,10 +11,9 @@ import {
DropdownMenuTrigger
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/NavBar/Navbar";
export const PersonalSettingsLayout = () => {
const { t } = useTranslation();
@@ -27,7 +26,7 @@ export const PersonalSettingsLayout = () => {
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div className="flex-grow">
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}>
<Link to="/organization/projects">
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
Back to organization
@@ -0,0 +1,103 @@
import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
export const PkiManagerLayout = () => {
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
return (
<>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
PKI Manager
</div>
<div className="flex-1">
<Menu>
<Link
to="/projects/$projectId/cert-manager/subscribers"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Subscribers</MenuItem>}
</Link>
<Link
to="/projects/$projectId/cert-manager/certificate-templates"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}> Certificate Templates</MenuItem>
)}
</Link>
<Link
to="/projects/$projectId/cert-manager/certificates"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}> Certificates</MenuItem>}
</Link>
<Link
to="/projects/$projectId/cert-manager/certificate-authorities"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}> Certificates Authorities</MenuItem>
)}
</Link>
<Link
to="/projects/$projectId/cert-manager/alerting"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Alerting</MenuItem>}
</Link>
{
// <Link
// to="/projects/$projectId/cert-manager/settings"
// params={{
// projectId: currentWorkspace.id
// }}
// >
// {({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
// </Link>
}
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
<Outlet />
</div>
</div>
</div>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
{` ${t("common.no-mobile")} `}
</p>
</div>
</>
);
};
@@ -0,0 +1 @@
export { PkiManagerLayout } from "./PkiManagerLayout";
@@ -0,0 +1,53 @@
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
export const ProjectGeneralLayout = () => {
const { currentWorkspace } = useWorkspace();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
Project Overview
</div>
<div className="flex-1">
<Menu>
<Link
to="/projects/$projectId/access-management"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Access Control</MenuItem>}
</Link>
<Link
to="/projects/$projectId/settings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Project Settings</MenuItem>}
</Link>
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
<Outlet />
</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { ProjectGeneralLayout } from "./ProjectGeneralLayout";
@@ -1,401 +1,317 @@
import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { faDotCircle, faMobile, faWindowMaximize } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet, useRouterState } from "@tanstack/react-router";
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import { ShouldWrap } from "@app/components/utilities/ShouldWrapComponent";
import {
Badge,
BreadcrumbContainer,
Divider,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Lottie,
Menu,
MenuGroup,
MenuItem,
TBreadcrumbFormat
Tooltip
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription,
useWorkspace
} from "@app/context";
import { ProjectPermissionSecretScanningFindingActions } from "@app/context/ProjectPermissionContext/types";
import {
useGetAccessRequestsCount,
useGetSecretApprovalRequestCount,
useGetSecretRotations
} from "@app/hooks/api";
import { useGetSecretScanningUnresolvedFindingCount } from "@app/hooks/api/secretScanningV2";
import { useProjectPermission, useWorkspace } from "@app/context";
import { getCurrentProductFromUrl } from "@app/helpers/project";
import { useLocalStorageState } from "@app/hooks";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { AssumePrivilegeModeBanner } from "./components/AssumePrivilegeModeBanner";
import { ProjectSelect } from "./components/ProjectSelect";
enum SidebarStyle {
Expanded = "expanded",
Collapsed = "collapsed",
ExpandOnHover = "expand-on-hover"
}
const MIN_SIDEBAR_SIZE = "55px";
const MAX_SIDEBAR_SIZE = "220px";
// This is a generic layout shared by all types of projects.
// If the product layout differs significantly, create a new layout as needed.
export const ProjectLayout = () => {
const location = useLocation();
const { currentWorkspace } = useWorkspace();
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
const { permission } = useProjectPermission();
const [sidebarStyle, setSidebarStyle] = useLocalStorageState(
"project-sidebar-style",
SidebarStyle.ExpandOnHover
);
const { t } = useTranslation();
const { assumedPrivilegeDetails } = useProjectPermission();
const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const { subscription } = useSubscription();
const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager;
const isCertManager = currentWorkspace?.type === ProjectType.CertificateManager;
const isCmek = currentWorkspace?.type === ProjectType.KMS;
const isSSH = currentWorkspace?.type === ProjectType.SSH;
const isSecretScanning = currentWorkspace?.type === ProjectType.SecretScanning;
const minSidebarWidth =
sidebarStyle === SidebarStyle.Expanded ? MAX_SIDEBAR_SIZE : MIN_SIDEBAR_SIZE;
const maxSidebarWidth =
sidebarStyle === SidebarStyle.Collapsed ? MIN_SIDEBAR_SIZE : MAX_SIDEBAR_SIZE;
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
workspaceId,
options: { enabled: isSecretManager }
});
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({
projectSlug,
options: { enabled: isSecretManager }
});
// we only show the secret rotations v1 tab if they have existing rotations
const { data: secretRotations } = useGetSecretRotations({
workspaceId,
options: {
enabled: isSecretManager && Boolean(subscription.secretRotation),
refetchOnMount: false
}
});
const pendingRequestsCount =
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(workspaceId, {
enabled:
isSecretScanning &&
subscription.secretScanning &&
permission.can(
ProjectPermissionSecretScanningFindingActions.Read,
ProjectPermissionSub.SecretScanningFindings
),
refetchInterval: 30000
});
const currentProductType = getCurrentProductFromUrl(location.pathname);
const isSecretManager = currentProductType === ProjectType.SecretManager;
const isPki = currentProductType === ProjectType.CertificateManager;
const isKms = currentProductType === ProjectType.KMS;
const isSsh = currentProductType === ProjectType.SSH;
const isSecretScanning = currentProductType === ProjectType.SecretScanning;
return (
<>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<div
className="dark relative hidden w-full flex-col overflow-x-hidden md:flex"
style={{
height: "calc(100vh - 3rem)",
paddingLeft: !(sidebarStyle === SidebarStyle.Expanded) ? MIN_SIDEBAR_SIZE : "0px"
}}
>
<div className="flex flex-grow flex-col overflow-y-auto overflow-x-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
style={{ width: minSidebarWidth, height: "calc(100vh - 3rem)" }}
whileHover={{
width: maxSidebarWidth
}}
className={twMerge(
"dark group z-10 w-full overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60",
!(sidebarStyle === SidebarStyle.Expanded) && "absolute bottom-0 left-0"
)}
>
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div>
<ProjectSelect />
<div className="px-1">
<Menu>
<MenuGroup title="Main Menu">
<nav className="items-between flex h-full flex-col justify-between">
<Menu>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="Secret Manager"
position="right"
>
<Link
to="/projects/$projectId/secret-manager/overview"
params={{ projectId: currentWorkspace.id }}
>
<MenuItem
className="relative flex items-center gap-2 overflow-hidden rounded-none"
isSelected={isSecretManager}
leftIcon={<Lottie className="inline-block h-6 w-6 shrink-0" icon="vault" />}
>
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
{t("nav.menu.secrets")}
</MenuItem>
)}
</Link>
)}
{isCertManager && (
<>
<Link
to={
`/${ProjectType.CertificateManager}/$projectId/subscribers` as const
}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="pki-subscriber">
Subscribers
</MenuItem>
)}
</Link>
<Link
to={
`/${ProjectType.CertificateManager}/$projectId/certificate-templates` as const
}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem
iconMode="reverse"
isSelected={isActive}
icon="pki-template"
>
Certificate Templates
</MenuItem>
)}
</Link>
<Link
to={
`/${ProjectType.CertificateManager}/$projectId/certificates` as const
}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="certificate">
Certificates
</MenuItem>
)}
</Link>
<Link
to={
`/${ProjectType.CertificateManager}/$projectId/certificate-authorities` as const
}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="certificate-authority">
Certificate Authorities
</MenuItem>
)}
</Link>
<Link
to={`/${ProjectType.CertificateManager}/$projectId/alerting` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="notification-bell">
Alerting
</MenuItem>
)}
</Link>
</>
)}
{isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/kmip` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="key-user" iconMode="reverse">
KMIP
</MenuItem>
)}
</Link>
)}
{isSSH && (
<>
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="server">
Hosts
</MenuItem>
)}
</Link>
{/* <Link
to={`/${ProjectType.SSH}/$projectId/certificates` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="certificate" iconMode="reverse">
Certificates
</MenuItem>
)}
</Link> */}
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) =>
isAllowed && (
<Link
to={`/${ProjectType.SSH}/$projectId/cas` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem
isSelected={isActive}
icon="certificate-authority"
iconMode="reverse"
>
Certificate Authorities
</MenuItem>
)}
</Link>
)
}
</ProjectPermissionCan>
</>
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
Secret Manager
</MenuItem>
</Link>
</ShouldWrap>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="PKI Manager"
position="right"
>
<Link
to="/projects/$projectId/cert-manager/subscribers"
params={{ projectId: currentWorkspace.id }}
>
<MenuItem
className="relative flex items-center gap-2 overflow-hidden rounded-none"
isSelected={isPki}
leftIcon={<Lottie className="inline-block h-6 w-6 shrink-0" icon="note" />}
>
{isPki && <div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />}
PKI Manager
</MenuItem>
</Link>
</ShouldWrap>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="KMS"
position="right"
>
<Link
to="/projects/$projectId/kms/overview"
params={{ projectId: currentWorkspace.id }}
>
<MenuItem
className="relative flex items-center gap-2 overflow-hidden rounded-none"
isSelected={isKms}
leftIcon={<Lottie className="inline-block h-6 w-6 shrink-0" icon="unlock" />}
>
{isKms && <div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />}
KMS
</MenuItem>
</Link>
</ShouldWrap>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="SSH"
position="right"
>
<Link
to="/projects/$projectId/ssh/overview"
params={{ projectId: currentWorkspace.id }}
>
<MenuItem
className="relative flex items-center gap-2 overflow-hidden rounded-none"
isSelected={isSsh}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="terminal" />
}
>
{isSsh && <div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />}
SSH
</MenuItem>
</Link>
</ShouldWrap>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="Secret Scanning"
position="right"
>
<Link
to="/projects/$projectId/secret-scanning/data-sources"
params={{ projectId: currentWorkspace.id }}
>
<MenuItem
className="relative flex items-center gap-2 overflow-hidden rounded-none"
isSelected={isSecretScanning}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="secret-scan" />
}
>
{isSecretScanning && (
<Link
to={`/${ProjectType.SecretScanning}/$projectId/data-sources` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="blocks">
Data Sources
</MenuItem>
)}
</Link>
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
{isSecretScanning && (
<Link
to={`/${ProjectType.SecretScanning}/$projectId/findings` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="search">
<div className="flex w-full items-center justify-between">
<span>Findings</span>
{Boolean(unresolvedFindings) && (
<Badge variant="primary" className="mr-2">
{unresolvedFindings}
</Badge>
)}
</div>
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
{t("nav.menu.integrations")}
</MenuItem>
)}
</Link>
)}
{isSecretManager && Boolean(secretRotations?.length) && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="rotation">
Secret Rotation
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/approval` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="circular-check">
Approvals
{Boolean(
secretApprovalReqCount?.open ||
accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
</MenuItem>
)}
</Link>
)}
</MenuGroup>
<MenuGroup title="Other">
<Link
to={`/${currentWorkspace.type}/$projectId/access-management` as const}
params={{
projectId: currentWorkspace.id
}}
Secret Scanning
</MenuItem>
</Link>
</ShouldWrap>
</Menu>
<Divider />
<Menu>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="Access Control"
position="right"
>
<Link
to="/projects/$projectId/access-management"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden rounded-none"
isSelected={isActive}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="groups" />
}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
</Link>
<Link
to={`/${currentWorkspace.type}/$projectId/settings` as const}
params={{
projectId: currentWorkspace.id
}}
Access Control
</MenuItem>
)}
</Link>
</ShouldWrap>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="Project Settings"
position="right"
>
<Link
to="/projects/$projectId/settings"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden rounded-none"
isSelected={isActive}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="settings-cog" />
}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
{t("nav.menu.project-settings")}
</MenuItem>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
</Link>
</MenuGroup>
</Menu>
</div>
</div>
Project Settings
</MenuItem>
)}
</Link>
</ShouldWrap>
</Menu>
<div className="flex-grow" />
<Menu>
<DropdownMenu>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="Sidebar Control"
position="right"
>
<DropdownMenuTrigger className="w-full">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faWindowMaximize}
flip="vertical"
/>
}
>
Sidebar Control
</MenuItem>
</DropdownMenuTrigger>
</ShouldWrap>
<DropdownMenuContent>
<DropdownMenuItem
iconPos="right"
icon={
sidebarStyle === SidebarStyle.Expanded && (
<FontAwesomeIcon icon={faDotCircle} size="sm" />
)
}
onClick={() => setSidebarStyle(SidebarStyle.Expanded)}
>
Expanded
</DropdownMenuItem>
<DropdownMenuItem
iconPos="right"
icon={
sidebarStyle === SidebarStyle.Collapsed && (
<FontAwesomeIcon icon={faDotCircle} size="sm" />
)
}
onClick={() => setSidebarStyle(SidebarStyle.Collapsed)}
>
Collapsed
</DropdownMenuItem>
<DropdownMenuItem
iconPos="right"
icon={
sidebarStyle === SidebarStyle.ExpandOnHover && (
<FontAwesomeIcon icon={faDotCircle} size="sm" />
)
}
onClick={() => setSidebarStyle(SidebarStyle.ExpandOnHover)}
>
Expand on hover
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</Menu>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]">
{breadcrumbs ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
) : null}
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
@@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "@app/components/v2";
import { useProjectPermission, useWorkspace } from "@app/context";
import { getCurrentProductFromUrl, getProjectHomePage } from "@app/helpers/project";
import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
@@ -36,20 +37,11 @@ export const AssumePrivilegeModeBanner = () => {
},
{
onSuccess: () => {
let page: string;
const url = getProjectHomePage(
getCurrentProductFromUrl(window.location.href) || ProjectType.SecretManager
);
switch (currentWorkspace.type) {
case ProjectType.SecretScanning:
page = "data-sources";
break;
case ProjectType.CertificateManager:
page = "subscribers";
break;
default:
page = "overview";
}
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/${page}`;
window.location.href = url.replace("$projectId", currentWorkspace.id);
}
}
);
@@ -0,0 +1,121 @@
import { ComponentPropsWithRef, ElementType, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { twMerge } from "tailwind-merge";
import { MenuItemProps } from "@app/components/v2";
export const MenuIconButton = <T extends ElementType = "button">({
children,
icon,
className,
isDisabled,
isSelected,
as: Item = "div",
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef,
lottieIconMode = "forward",
...props
}: MenuItemProps<T> &
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null);
return (
<div className="px-1">
<Item
type="button"
role="menuitem"
className={twMerge(
"group relative my-1 flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
isSelected && "bg-bunker-8g0 hover:bg-mineshaft-500",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
ref={inputRef}
{...props}
>
<div
className={`${isSelected ? "opacity-100" : "opacity-0"} absolute left-0 h-full w-0.5 rounded-bl rounded-tl bg-primary transition-all duration-150`}
/>
{icon && (
<div className="my-auto mb-2 h-6 w-6">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
mode={lottieIconMode}
/>
</div>
)}
<div
className="flex-grow justify-center break-words text-center"
style={{ fontSize: "10px" }}
>
{children}
</div>
</Item>
</div>
);
};
// export const MenuIconButton = <T extends ElementType = "button">({
// children,
// icon,
// className,
// isDisabled,
// isSelected,
// as: Item = "div",
// description,
// // wrapping in forward ref with generic component causes the loss of ts definitions on props
// inputRef,
// lottieIconMode = "forward",
// ...props
// }: MenuItemProps<T> &
// ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
// const iconRef = useRef<DotLottie | null>(null);
// return (
// <div className="px-1">
// <Item
// type="button"
// role="menuitem"
// className={twMerge(
// "group relative my-1 flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
// isSelected && "bg-mineshaft-600 hover:bg-mineshaft-700",
// isDisabled && "cursor-not-allowed hover:bg-transparent",
// className
// )}
// onMouseEnter={() => iconRef.current?.play()}
// onMouseLeave={() => iconRef.current?.stop()}
// ref={inputRef}
// {...props}
// >
// <div
// className={`${"opacity-0"} absolute left-0 h-full w-1 bg-primary transition-all duration-150`}
// />
// {icon && (
// <div className="my-auto mb-2 h-6 w-6">
// <DotLottieReact
// dotLottieRefCallback={(el) => {
// iconRef.current = el;
// }}
// src={`/lotties/${icon}.json`}
// loop
// className="h-full w-full"
// mode={lottieIconMode}
// />
// </div>
// )}
// <div
// className="flex-grow justify-center break-words text-center"
// style={{ fontSize: "10px" }}
// >
// {children}
// </div>
// </Item>
// </div>
// );
// };
@@ -0,0 +1,65 @@
import { ComponentPropsWithRef, ElementType, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { twMerge } from "tailwind-merge";
import { MenuItemProps } from "@app/components/v2";
export const MenuIconButton = <T extends ElementType = "button">({
children,
icon,
className,
isDisabled,
isSelected,
as: Item = "div",
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef,
lottieIconMode = "forward",
...props
}: MenuItemProps<T> &
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null);
return (
<div>
<Item
type="button"
role="menuitem"
className={twMerge(
"group relative flex w-full cursor-pointer flex-col items-center justify-center p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
isSelected && "rounded-none bg-bunker-800 hover:bg-mineshaft-600",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
ref={inputRef}
{...props}
>
<div
className={`${
isSelected ? "opacity-100" : "opacity-0"
} absolute left-0 h-full w-0.5 bg-primary transition-all duration-150`}
/>
{icon && (
<div className="my-auto mb-2 h-6 w-6">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
mode={lottieIconMode}
/>
</div>
)}
<div
className="flex-grow justify-center break-words text-center"
style={{ fontSize: "10px" }}
>
{children}
</div>
</Item>
</div>
);
};
@@ -0,0 +1 @@
export { MenuIconButton } from "./MenuIconButton";
@@ -1,15 +1,29 @@
import { useMemo } from "react";
import { components, MenuProps, OptionProps } from "react-select";
import { useMemo, useState } from "react";
import { faStar } from "@fortawesome/free-regular-svg-icons";
import { faChevronRight, faPlus, faStar as faSolidStar } from "@fortawesome/free-solid-svg-icons";
import {
faCheck,
faCube,
faMagnifyingGlass,
faPlus,
faSort,
faStar as faSolidStar
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Link, linkOptions } from "@tanstack/react-router";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { NewProjectModal } from "@app/components/projects";
import { Button, FilterableSelect } from "@app/components/v2";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Input,
Tooltip
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
@@ -17,49 +31,39 @@ import {
useSubscription,
useWorkspace
} from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
import { getCurrentProductFromUrl, getProjectHomePage } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
import { Workspace } from "@app/hooks/api/workspace/types";
type TWorkspaceWithFaveProp = Workspace & { isFavorite: boolean };
const ProjectsMenu = ({ children, ...props }: MenuProps<TWorkspaceWithFaveProp>) => {
return (
<components.Menu {...props}>
{children}
<hr className="mb-2 h-px border-0 bg-mineshaft-500" />
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Workspace}>
{(isAllowed) => (
<Button
className="w-full bg-mineshaft-700 pt-2 text-bunker-200"
colorSchema="primary"
variant="outline_bg"
size="xs"
isDisabled={!isAllowed}
onClick={() => props.clearValue()}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
)}
</OrgPermissionCan>
</components.Menu>
);
};
const ProjectOption = ({
isSelected,
children,
data,
...props
}: OptionProps<TWorkspaceWithFaveProp>) => {
// TODO(pta): add search to project select
export const ProjectSelect = () => {
const [searchProject, setSearchProject] = useState("");
const { currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization();
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
const { data: workspaces = [] } = useGetUserWorkspaces();
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg.id);
const { subscription } = useSubscription();
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
const addProjectToFavorites = async (projectId: string) => {
try {
await updateUserProjectFavorites({
orgId: currentOrg!.id,
projectFavorites: [...(projectFavorites || []), projectId]
});
} catch {
createNotification({
text: "Failed to add project to favorites.",
type: "error"
});
}
};
const removeProjectFromFavorites = async (projectId: string) => {
try {
await updateUserProjectFavorites({
@@ -74,63 +78,6 @@ const ProjectOption = ({
}
};
const addProjectToFavorites = async (projectId: string) => {
try {
await updateUserProjectFavorites({
orgId: currentOrg!.id,
projectFavorites: [...(projectFavorites || []), projectId]
});
} catch {
createNotification({
text: "Failed to add project to favorites.",
type: "error"
});
}
};
return (
<components.Option
isSelected={isSelected}
data={data}
{...props}
className={twMerge(props.className, isSelected && "bg-mineshaft-500")}
>
<div className="flex w-full items-center">
{isSelected && (
<FontAwesomeIcon className="mr-2 text-primary" icon={faChevronRight} size="xs" />
)}
<p className="truncate">{children}</p>
{data.isFavorite ? (
<FontAwesomeIcon
icon={faSolidStar}
className="ml-auto text-sm text-yellow-600 hover:text-mineshaft-400"
onClick={async (e) => {
e.stopPropagation();
await removeProjectFromFavorites(data.id);
}}
/>
) : (
<FontAwesomeIcon
icon={faStar}
className="ml-auto text-sm text-mineshaft-400 hover:text-mineshaft-300"
onClick={async (e) => {
e.stopPropagation();
await addProjectToFavorites(data.id);
}}
/>
)}
</div>
</components.Option>
);
};
export const ProjectSelect = () => {
const { currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization();
const { data: workspaces = [] } = useGetUserWorkspaces({ type: currentWorkspace.type });
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg.id!);
const { subscription } = useSubscription();
const isAddingProjectsAllowed = subscription?.workspaceLimit
? subscription.workspacesUsed < subscription.workspaceLimit
: true;
@@ -140,7 +87,7 @@ export const ProjectSelect = () => {
"upgradePlan"
] as const);
const { options, value } = useMemo(() => {
const projects = useMemo(() => {
const projectOptions = workspaces
.map((w): Workspace & { isFavorite: boolean } => ({
...w,
@@ -148,70 +95,136 @@ export const ProjectSelect = () => {
}))
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite));
const currentOption = projectOptions.find((option) => option.id === currentWorkspace?.id);
if (!currentOption) {
return {
options: projectOptions,
value: null
};
}
return {
options: [
currentOption,
...projectOptions.filter((option) => option.id !== currentOption.id)
],
value: currentOption
};
return projectOptions;
}, [workspaces, projectFavorites, currentWorkspace]);
return (
<div className="mt-2 w-full p-3">
<p className="mb-1 ml-1.5 text-xs font-semibold uppercase text-gray-400">
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"}
</p>
<FilterableSelect
className="text-sm"
value={value}
filterOption={(option, inputValue) =>
option.data.name.toLowerCase().includes(inputValue.toLowerCase())
}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
onChange={(newValue) => {
// hacky use of null as indication to create project
if (!newValue) {
if (isAddingProjectsAllowed) {
handlePopUpOpen("addNewWs");
} else {
handlePopUpOpen("upgradePlan");
}
return;
}
const project = newValue as TWorkspaceWithFaveProp;
// todo(akhi): this is not using react query because react query in overview is throwing error when envs are not exact same count
// to reproduce change this back to router.push and switch between two projects with different env count
// look into this on dashboard revamp
window.location.assign(`/${project.type}/${project.id}/overview`);
}}
options={options}
components={{
Option: ProjectOption,
Menu: ProjectsMenu
}}
/>
<div className="flex w-full items-center gap-2">
<DropdownMenu modal={false}>
<Link
to={getProjectHomePage(
getCurrentProductFromUrl(window.location.href) || currentWorkspace.defaultProduct
)}
params={{
projectId: currentWorkspace.id
}}
>
<div className="flex cursor-pointer items-center gap-2 text-sm text-white duration-100 hover:text-primary-400">
<div>
<FontAwesomeIcon icon={faCube} className="text-xs" />
</div>
<Tooltip content={currentWorkspace.name}>
<div className="max-w-32 overflow-hidden text-ellipsis whitespace-nowrap">
{currentWorkspace?.name}
</div>
</Tooltip>
</div>
</Link>
<DropdownMenuTrigger asChild>
<div>
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="switch-org"
className="px-2 py-1"
>
<FontAwesomeIcon icon={faSort} className="text-xs text-bunker-300" />
</IconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs capitalize text-mineshaft-400">Projects</div>
<div className="mb-1 border-b border-b-mineshaft-600 py-1 pb-1">
<Input
value={searchProject}
onChange={(evt) => setSearchProject(evt.target.value || "")}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
size="xs"
variant="plain"
placeholder="Search projects"
/>
</div>
<div className="thin-scrollbar max-h-80 overflow-auto">
{projects
?.filter((el) => el.name?.toLowerCase().includes(searchProject.toLowerCase()))
?.map((workspace) => {
return (
<DropdownMenuItem
key={workspace.id}
onClick={async () => {
// todo(akhi): this is not using react query because react query in overview is throwing error when envs are not exact same count
// to reproduce change this back to router.push and switch between two projects with different env count
// look into this on dashboard revamp
const url = linkOptions({
to: getProjectHomePage(workspace.defaultProduct),
params: {
projectId: workspace.id
}
});
window.location.assign(url.to.replaceAll("$projectId", workspace.id));
}}
icon={
currentWorkspace?.id === workspace.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
>
<div className="flex items-center">
<div className="flex flex-1 items-center justify-between overflow-hidden">
<Tooltip content={workspace.name}>
<div className="max-w-40 overflow-hidden truncate whitespace-nowrap">
{workspace.name}
</div>
</Tooltip>
</div>
<div>
<FontAwesomeIcon
icon={workspace.isFavorite ? faSolidStar : faStar}
className="text-sm text-yellow-600 hover:text-mineshaft-400"
onClick={async (e) => {
e.stopPropagation();
await (
workspace.isFavorite
? removeProjectFromFavorites
: addProjectToFavorites
)(workspace.id);
}}
/>
</div>
</div>
</DropdownMenuItem>
);
})}
</div>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Workspace}>
{(isAllowed) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faPlus} />}
onClick={() =>
handlePopUpOpen(isAddingProjectsAllowed ? "addNewWs" : "upgradePlan")
}
>
New Project
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You have exceeded the number of projects allowed on the free plan."
/>
<NewProjectModal
isOpen={popUp.addNewWs.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
projectType={currentWorkspace?.type || ProjectType.SecretManager}
/>
</div>
);
@@ -14,7 +14,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { useOrganization, useUser, useWorkspace } from "@app/context";
import { useOrganization, useUser } from "@app/context";
import { useGetOrganizations, useLogoutUser } from "@app/hooks/api";
import { AuthMethod } from "@app/hooks/api/users/types";
@@ -24,7 +24,6 @@ type Prop = {
export const SidebarHeader = ({ onChangeOrg }: Prop) => {
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const { user } = useUser();
const navigate = useNavigate();
const { data: orgs } = useGetOrganizations();
@@ -42,7 +41,7 @@ export const SidebarHeader = ({ onChangeOrg }: Prop) => {
return (
<div className="flex h-12 cursor-default items-center px-3 pt-6">
<Link to={`/organization/${currentWorkspace.type}/overview` as const}>
<Link to="/organization/projects">
<div className="pl-1 pr-2 text-mineshaft-400 duration-200 hover:text-mineshaft-100">
<FontAwesomeIcon icon={faArrowLeft} />
</div>
@@ -0,0 +1,132 @@
import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Badge, Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useGetAccessRequestsCount,
useGetSecretApprovalRequestCount,
useGetSecretRotations
} from "@app/hooks/api";
export const SecretManagerLayout = () => {
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
workspaceId
});
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({
projectSlug
});
// we only show the secret rotations v1 tab if they have existing rotations
const { data: secretRotations } = useGetSecretRotations({
workspaceId,
options: {
refetchOnMount: false
}
});
const pendingRequestsCount =
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
return (
<>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
Secrets Manager
</div>
<div className="flex-1">
<Menu>
<Link
to="/projects/$projectId/secret-manager/overview"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>{t("nav.menu.secrets")}</MenuItem>
)}
</Link>
<Link
to="/projects/$projectId/secret-manager/integrations"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>{t("nav.menu.integrations")}</MenuItem>
)}
</Link>
{Boolean(secretRotations?.length) && (
<Link
to="/projects/$projectId/secret-manager/secret-rotation"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Secret Rotation</MenuItem>}
</Link>
)}
<Link
to="/projects/$projectId/secret-manager/approval"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
Approvals
{Boolean(
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
</MenuItem>
)}
</Link>
<Link
to="/projects/$projectId/secret-manager/settings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
</Link>
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
<Outlet />
</div>
</div>
</div>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
{` ${t("common.no-mobile")} `}
</p>
</div>
</>
);
};
@@ -0,0 +1 @@
export { SecretManagerLayout } from "./SecretManagerLayout";
@@ -0,0 +1,61 @@
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
export const SecretScanningLayout = () => {
const { currentWorkspace } = useWorkspace();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
Secret Scanning
</div>
<div className="flex-1">
<Menu>
<Link
to="/projects/$projectId/secret-scanning/data-sources"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}> Data Sources</MenuItem>}
</Link>
<Link
to="/projects/$projectId/secret-scanning/findings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Findings</MenuItem>}
</Link>
<Link
to="/projects/$projectId/secret-scanning/settings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
</Link>
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
<Outlet />
</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { SecretScanningLayout } from "./SecretScanningLayout";
@@ -0,0 +1,71 @@
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Menu, MenuItem } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
export const SshLayout = () => {
const { currentWorkspace } = useWorkspace();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">SSH</div>
<div className="flex-1">
<Menu>
<Link
to="/projects/$projectId/ssh/overview"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Hosts</MenuItem>}
</Link>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) =>
isAllowed && (
<Link
to="/projects/$projectId/ssh/cas"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>Certificate Authorities</MenuItem>
)}
</Link>
)
}
</ProjectPermissionCan>
<Link
to="/projects/$projectId/ssh/settings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
</Link>
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
<Outlet />
</div>
</div>
</div>
);
};
+1
View File
@@ -0,0 +1 @@
export { SshLayout } from "./SshLayout";
+3 -3
View File
@@ -5,7 +5,7 @@ import lottieWasmUrl from "@lottiefiles/dotlottie-web/dist/dotlottie-player.wasm
import { createRouter, RouterProvider } from "@tanstack/react-router";
import NProgress from "nprogress";
import { ContentLoader } from "./components/v2";
import { Lottie } from "./components/v2";
import { queryClient } from "./hooks/api/reactQuery";
import { ErrorPage } from "./pages/public/ErrorPage/ErrorPage";
import { NotFoundPage } from "./pages/public/NotFoundPage/NotFoundPage";
@@ -62,8 +62,8 @@ const router = createRouter({
routeTree,
context: { serverConfig: null, queryClient },
defaultPendingComponent: () => (
<div className="bg-bunker-800">
<ContentLoader />
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
<Lottie icon="infisical_loading" className="h-32 w-32" />
</div>
),
defaultNotFoundComponent: NotFoundPage,
+1 -1
View File
@@ -7,7 +7,7 @@ export const Route = createFileRoute("/_authenticate/_inject-org-details/admin/_
beforeLoad: ({ context }) => {
if (!context.user.superAdmin) {
throw redirect({
to: "/organization/secret-manager/overview"
to: "/organization/projects"
});
}
}
@@ -4,7 +4,6 @@ import { useServerConfig } from "@app/context";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { queryClient } from "@app/hooks/api/reactQuery";
import { userKeys } from "@app/hooks/api/users";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: string) => {
const userOrgs = await fetchOrganizations();
@@ -13,7 +12,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
if (organizationId) {
localStorage.setItem("orgData.id", organizationId);
navigate({ to: "/organization/secret-manager/overview" });
navigate({ to: "/organization/projects" });
return;
}
@@ -21,7 +20,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
// user is part of at least 1 non-auth enforced org
const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id;
localStorage.setItem("orgData.id", userOrg);
navigate({ to: `/organization/${ProjectType.SecretManager}/overview` as const });
navigate({ to: "/organization/projects" });
} else {
// user is not part of any non-auth enforced orgs
localStorage.removeItem("orgData.id");

Some files were not shown because too many files have changed in this diff Show More