@@ -15,7 +15,7 @@ import {
1515 json ,
1616 redirectDocument ,
1717} from "@remix-run/server-runtime" ;
18- import { useState } from "react" ;
18+ import { useMemo , useState } from "react" ;
1919import { typedjson , useTypedLoaderData } from "remix-typedjson" ;
2020import { z } from "zod" ;
2121import { InlineCode } from "~/components/code/InlineCode" ;
@@ -199,6 +199,43 @@ export default function Page() {
199199 const project = useProject ( ) ;
200200 const environment = useEnvironment ( ) ;
201201
202+ // Add isFirst and isLast to each environment variable
203+ // They're set based on if they're the first or last time that `key` has been seen in the list
204+ const groupedEnvironmentVariables = useMemo ( ( ) => {
205+ // Create a map to track occurrences of each key
206+ const keyOccurrences = new Map < string , number > ( ) ;
207+
208+ // First pass: count total occurrences of each key
209+ environmentVariables . forEach ( ( variable ) => {
210+ keyOccurrences . set ( variable . key , ( keyOccurrences . get ( variable . key ) || 0 ) + 1 ) ;
211+ } ) ;
212+
213+ // Second pass: add isFirstTime, isLastTime, and occurrences flags
214+ const seenKeys = new Set < string > ( ) ;
215+ const currentOccurrences = new Map < string , number > ( ) ;
216+
217+ return environmentVariables . map ( ( variable ) => {
218+ // Track current occurrence number for this key
219+ const currentCount = ( currentOccurrences . get ( variable . key ) || 0 ) + 1 ;
220+ currentOccurrences . set ( variable . key , currentCount ) ;
221+
222+ const totalOccurrences = keyOccurrences . get ( variable . key ) || 1 ;
223+ const isFirstTime = ! seenKeys . has ( variable . key ) ;
224+ const isLastTime = currentCount === totalOccurrences ;
225+
226+ if ( isFirstTime ) {
227+ seenKeys . add ( variable . key ) ;
228+ }
229+
230+ return {
231+ ...variable ,
232+ isFirstTime,
233+ isLastTime,
234+ occurences : totalOccurrences ,
235+ } ;
236+ } ) ;
237+ } , [ environmentVariables ] ) ;
238+
202239 return (
203240 < PageContainer >
204241 < NavBar >
@@ -245,47 +282,79 @@ export default function Page() {
245282 </ TableRow >
246283 </ TableHeader >
247284 < TableBody >
248- { environmentVariables . length > 0 ? (
249- environmentVariables . map ( ( variable ) => (
250- < TableRow key = { `${ variable . id } -${ variable . environment . id } ` } >
251- < TableCell >
252- < CopyableText value = { variable . key } className = "font-mono" />
253- </ TableCell >
254- < TableCell >
255- { variable . isSecret ? (
256- < SimpleTooltip
257- button = {
258- < div className = "flex items-center gap-x-1.5" >
259- < LockClosedIcon className = "size-3 text-text-dimmed" />
260- < span className = "text-sm text-text-dimmed" > Secret</ span >
261- </ div >
262- }
263- content = "This variable is secret and cannot be revealed."
264- />
265- ) : (
266- < ClipboardField
267- secure = { ! revealAll }
268- value = { variable . value }
269- variant = { "secondary/small" }
270- fullWidth = { true }
271- />
272- ) }
273- </ TableCell >
274-
275- < TableCell >
276- < EnvironmentCombo environment = { variable . environment } className = "text-sm" />
277- </ TableCell >
278- < TableCellMenu
279- isSticky
280- hiddenButtons = {
281- < >
282- < EditEnvironmentVariablePanel variable = { variable } revealAll = { revealAll } />
283- < DeleteEnvironmentVariableButton variable = { variable } />
284- </ >
285+ { groupedEnvironmentVariables . length > 0 ? (
286+ groupedEnvironmentVariables . map ( ( variable ) => {
287+ let cellClassName = "" ;
288+ let borderedCellClassName = "" ;
289+
290+ if ( variable . occurences > 1 ) {
291+ cellClassName = "py-1" ;
292+ borderedCellClassName =
293+ "relative after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-grid-bright" ;
294+ if ( variable . isLastTime ) {
295+ cellClassName = "pt-1 pb-2" ;
296+ borderedCellClassName = "" ;
297+ } else if ( variable . isFirstTime ) {
298+ cellClassName = "pt-2 pb-1" ;
299+ }
300+ } else {
301+ cellClassName = "py-2" ;
302+ }
303+
304+ return (
305+ < TableRow
306+ key = { `${ variable . id } -${ variable . environment . id } ` }
307+ className = {
308+ variable . isLastTime ? "after:bg-charcoal-600" : "after:bg-transparent"
285309 }
286- />
287- </ TableRow >
288- ) )
310+ >
311+ < TableCell className = { cellClassName } >
312+ { variable . isFirstTime ? (
313+ < CopyableText value = { variable . key } className = "font-mono" />
314+ ) : null }
315+ </ TableCell >
316+ < TableCell
317+ className = { cn ( cellClassName , borderedCellClassName , "after:left-3" ) }
318+ >
319+ { variable . isSecret ? (
320+ < SimpleTooltip
321+ button = {
322+ < div className = "flex items-center gap-x-1.5" >
323+ < LockClosedIcon className = "size-3 text-text-dimmed" />
324+ < span className = "text-sm text-text-dimmed" > Secret</ span >
325+ </ div >
326+ }
327+ content = "This variable is secret and cannot be revealed."
328+ />
329+ ) : (
330+ < ClipboardField
331+ secure = { ! revealAll }
332+ value = { variable . value }
333+ variant = { "secondary/small" }
334+ fullWidth = { true }
335+ />
336+ ) }
337+ </ TableCell >
338+
339+ < TableCell className = { cn ( cellClassName , borderedCellClassName ) } >
340+ < EnvironmentCombo environment = { variable . environment } className = "text-sm" />
341+ </ TableCell >
342+ < TableCellMenu
343+ className = { cn ( cellClassName , borderedCellClassName ) }
344+ isSticky
345+ hiddenButtons = {
346+ < >
347+ < EditEnvironmentVariablePanel
348+ variable = { variable }
349+ revealAll = { revealAll }
350+ />
351+ < DeleteEnvironmentVariableButton variable = { variable } />
352+ </ >
353+ }
354+ />
355+ </ TableRow >
356+ ) ;
357+ } )
289358 ) : (
290359 < TableRow >
291360 < TableCell colSpan = { 4 } >
0 commit comments