diff --git a/lib/screens/common_widgets/envvar_span.dart b/lib/screens/common_widgets/envvar_span.dart index c9038d280..ee8a22e2f 100644 --- a/lib/screens/common_widgets/envvar_span.dart +++ b/lib/screens/common_widgets/envvar_span.dart @@ -27,9 +27,11 @@ class EnvVarSpan extends HookConsumerWidget { final showPopover = useState(false); final isMissingVariable = suggestion.isUnknown; - final String scope = isMissingVariable - ? 'unknown' - : getEnvironmentTitle(environments?[suggestion.environmentId]?.name); + final String scope = suggestion.environmentId == "Random" + ? "Random" + : isMissingVariable + ? 'unknown' + : getEnvironmentTitle(environments?[suggestion.environmentId]?.name); final colorScheme = Theme.of(context).colorScheme; var text = Text( diff --git a/lib/screens/envvar/editor_pane/fake_data_pane.dart b/lib/screens/envvar/editor_pane/fake_data_pane.dart new file mode 100644 index 000000000..bd1685eb2 --- /dev/null +++ b/lib/screens/envvar/editor_pane/fake_data_pane.dart @@ -0,0 +1,214 @@ +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/utils/fake_data_provider.dart'; + +class FakeDataProvidersPane extends ConsumerWidget { + const FakeDataProvidersPane({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: kBorderRadius12, + ), + margin: kP10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Fake Data Providers', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 10), + Text( + 'Use these placeholders in your API requests to generate random test data automatically.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 20), + Text( + 'How to use:', + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 5), + Text( + 'Type {{\$tagName}} in any field where you want to use random data.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 10), + Text( + 'For example: {{\$randomEmail}} will be replaced with a randomly generated email address.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 30), + Text( + 'Available Placeholders:', + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 20), + _buildFakeDataTable(context), + ], + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildFakeDataTable(BuildContext context) { + final fakeDataTags = [ + _FakeDataItem( + name: 'randomUsername', + description: 'Random username (e.g., user123, test_dev)', + example: FakeDataProvider.randomUsername(), + ), + _FakeDataItem( + name: 'randomEmail', + description: 'Random email address', + example: FakeDataProvider.randomEmail(), + ), + _FakeDataItem( + name: 'randomId', + description: 'Random numeric ID', + example: FakeDataProvider.randomId(), + ), + _FakeDataItem( + name: 'randomUuid', + description: 'Random UUID', + example: FakeDataProvider.randomUuid(), + ), + _FakeDataItem( + name: 'randomName', + description: 'Random full name', + example: FakeDataProvider.randomName(), + ), + _FakeDataItem( + name: 'randomPhone', + description: 'Random phone number', + example: FakeDataProvider.randomPhone(), + ), + _FakeDataItem( + name: 'randomAddress', + description: 'Random address', + example: FakeDataProvider.randomAddress(), + ), + _FakeDataItem( + name: 'randomDate', + description: 'Random date (YYYY-MM-DD)', + example: FakeDataProvider.randomDate(), + ), + _FakeDataItem( + name: 'randomDateTime', + description: 'Random date and time (ISO format)', + example: FakeDataProvider.randomDateTime(), + ), + _FakeDataItem( + name: 'randomBoolean', + description: 'Random boolean value (true/false)', + example: FakeDataProvider.randomBoolean(), + ), + _FakeDataItem( + name: 'randomNumber', + description: 'Random number between 0-1000', + example: FakeDataProvider.randomNumber(), + ), + _FakeDataItem( + name: 'randomJson', + description: 'Random JSON object with basic fields', + example: FakeDataProvider.randomJson(), + ), + ]; + + return Table( + columnWidths: const { + 0: FlexColumnWidth(1.2), + 1: FlexColumnWidth(2), + 2: FlexColumnWidth(1.5), + 3: FlexColumnWidth(0.5), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + border: TableBorder.all( + color: Theme.of(context).colorScheme.outline, + width: 1, + ), + children: [ + + TableRow( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + _buildTableCell(context, 'Placeholder', isHeader: true), + _buildTableCell(context, 'Description', isHeader: true), + _buildTableCell(context, 'Example Output', isHeader: true), + _buildTableCell(context, 'Copy', isHeader: true), + ], + ), + + for (var item in fakeDataTags) + TableRow( + children: [ + _buildTableCell(context, '{{\$${item.name}}}'), + _buildTableCell(context, item.description), + _buildTableCell(context, item.example), + _buildCopyButton(context, '{{\$${item.name}}}'), + ], + ), + ], + ); + } + + Widget _buildTableCell(BuildContext context, String text, {bool isHeader = false}) { + return Padding( + padding: const EdgeInsets.all(8), + child: Text( + text, + style: isHeader + ? Theme.of(context).textTheme.titleSmall + : Theme.of(context).textTheme.bodyMedium, + ), + ); + } + + Widget _buildCopyButton(BuildContext context, String textToCopy) { + return Padding( + padding: const EdgeInsets.all(4), + child: IconButton( + icon: const Icon(Icons.copy, size: 18), + onPressed: () { + Clipboard.setData(ClipboardData(text: textToCopy)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Copied $textToCopy to clipboard'), + duration: const Duration(seconds: 1), + ), + ); + }, + tooltip: 'Copy to clipboard', + ), + ); + } +} + +class _FakeDataItem { + final String name; + final String description; + final String example; + + _FakeDataItem({ + required this.name, + required this.description, + required this.example, + }); +} diff --git a/lib/screens/envvar/environment_editor.dart b/lib/screens/envvar/environment_editor.dart index 4ca07b570..8ea924120 100644 --- a/lib/screens/envvar/environment_editor.dart +++ b/lib/screens/envvar/environment_editor.dart @@ -6,10 +6,81 @@ import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import '../common_widgets/common_widgets.dart'; import './editor_pane/variables_pane.dart'; +import './editor_pane/fake_data_pane.dart'; + +final environmentEditorTabProvider = StateProvider((ref) => 0); class EnvironmentEditor extends ConsumerWidget { const EnvironmentEditor({super.key}); + Widget _buildTabBar(BuildContext context, WidgetRef ref) { + final selectedTab = ref.watch(environmentEditorTabProvider); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildTabButton( + context, + 'Environment Variables', + 0, + selectedTab == 0, + () => ref.read(environmentEditorTabProvider.notifier).state = 0, + ), + const SizedBox(width: 16), + _buildTabButton( + context, + 'Fake Data Providers', + 1, + selectedTab == 1, + () => ref.read(environmentEditorTabProvider.notifier).state = 1, + ), + ], + ); + } + + Widget _buildTabButton(BuildContext context, String title, int index, bool isSelected, VoidCallback onPressed) { + return ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: isSelected + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surfaceContainerLow, + foregroundColor: isSelected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface, + elevation: 0, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + child: Text(title), + ); + } + + Widget _buildTabContent(WidgetRef ref) { + final selectedTab = ref.watch(environmentEditorTabProvider); + + if (selectedTab == 0) { + return Column( + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 30), + Text("Variable"), + SizedBox(width: 30), + Text("Value"), + SizedBox(width: 40), + ], + ), + kHSpacer40, + const Divider(), + const Expanded(child: EditEnvironmentVariables()), + ], + ); + } else { + return const FakeDataProvidersPane(); + } + } + @override Widget build(BuildContext context, WidgetRef ref) { final id = ref.watch(selectedEnvironmentIdStateProvider); @@ -84,24 +155,13 @@ class EnvironmentEditor extends ConsumerWidget { borderRadius: kBorderRadius12, ), elevation: 0, - child: const Padding( + child: Padding( padding: kPv6, child: Column( children: [ - kHSpacer40, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox(width: 30), - Text("Variable"), - SizedBox(width: 30), - Text("Value"), - SizedBox(width: 40), - ], - ), - kHSpacer40, - Divider(), - Expanded(child: EditEnvironmentVariables()) + _buildTabBar(context, ref), + kHSpacer20, + Expanded(child: _buildTabContent(ref)) ], ), ), diff --git a/lib/utils/envvar_utils.dart b/lib/utils/envvar_utils.dart index c94f6eebc..448690811 100644 --- a/lib/utils/envvar_utils.dart +++ b/lib/utils/envvar_utils.dart @@ -1,5 +1,6 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:apidash/consts.dart'; +import 'fake_data_provider.dart'; String getEnvironmentTitle(String? name) { if (name == null || name.trim() == "") { @@ -41,6 +42,7 @@ String? substituteVariables( Map envVarMap, ) { if (input == null) return null; + if (envVarMap.keys.isEmpty) { return input; } @@ -151,9 +153,32 @@ EnvironmentVariableSuggestion getVariableStatus( ); } + // If not found in environments check if it's a random data generator + if (key.startsWith('\$')) { + final generatorType = key.substring(1); + final generator = FakeDataProvider.processFakeDataTag(generatorType); + if (generator != '{{generatorType}}') { + return EnvironmentVariableSuggestion( + environmentId: "Random", + variable: EnvironmentVariableModel( + key: key, + type: EnvironmentVariableType.variable, + value: generator, + enabled: true, + ), + isUnknown: false, + ); + } + } + return EnvironmentVariableSuggestion( - isUnknown: true, - environmentId: "unknown", - variable: EnvironmentVariableModel( - key: key, type: EnvironmentVariableType.variable, value: "unknown")); + isUnknown: true, + environmentId: "unknown", + variable: EnvironmentVariableModel( + key: key, + type: EnvironmentVariableType.variable, + value: "unknown", + // enabled: false, + ), + ); } diff --git a/lib/utils/fake_data_provider.dart b/lib/utils/fake_data_provider.dart new file mode 100644 index 000000000..6c5b84e7b --- /dev/null +++ b/lib/utils/fake_data_provider.dart @@ -0,0 +1,144 @@ +import 'dart:math'; + +/// A utility class that provides functions to generate fake data for API testing +class FakeDataProvider { + static final Random _random = Random(); + + static String randomUsername() { + final prefixes = ['user', 'test', 'dev', 'qa', 'admin', 'guest', 'john', 'jane']; + final suffixes = ['123', '2023', '_test', '_dev', '_admin', '_guest']; + + final prefix = prefixes[_random.nextInt(prefixes.length)]; + final suffix = _random.nextInt(10) > 5 ? suffixes[_random.nextInt(suffixes.length)] : ''; + + return '$prefix$suffix'; + } + + static String randomEmail() { + final usernames = ['john', 'jane', 'user', 'test', 'dev', 'admin', 'info', 'support']; + final domains = ['example.com', 'test.com', 'acme.org', 'email.net', 'company.io', 'service.dev']; + + final username = usernames[_random.nextInt(usernames.length)]; + final randomNum = _random.nextInt(1000); + final domain = domains[_random.nextInt(domains.length)]; + + return '$username$randomNum@$domain'; + } + + static String randomId() { + return _random.nextInt(10000).toString().padLeft(4, '0'); + } + + static String randomUuid() { + const chars = 'abcdef0123456789'; + final segments = [8, 4, 4, 4, 12]; //standard length of different segments of uuid + + final uuid = segments.map((segment) { + return List.generate(segment, (_) => chars[_random.nextInt(chars.length)]).join(); + }).join('-'); + + return uuid; + } + + static String randomName() { + final firstNames = ['John', 'Jane', 'Michael', 'Emily', 'David', 'Sarah', 'Robert', 'Lisa']; + final lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller', 'Davis', 'Wilson']; + + final firstName = firstNames[_random.nextInt(firstNames.length)]; + final lastName = lastNames[_random.nextInt(lastNames.length)]; + + return '$firstName $lastName'; + } + + static String randomPhone() { + final areaCode = (100 + _random.nextInt(900)).toString(); + final firstPart = (100 + _random.nextInt(900)).toString(); + final secondPart = (1000 + _random.nextInt(9000)).toString(); + + return '+1-$areaCode-$firstPart-$secondPart'; + } + + static String randomAddress() { + final streetNumbers = List.generate(100, (i) => (i + 1) * 10); + final streetNames = ['Main St', 'Oak Ave', 'Park Rd', 'Maple Dr', 'Pine Ln', 'Cedar Blvd']; + final cities = ['Springfield', 'Rivertown', 'Lakeside', 'Mountainview', 'Brookfield']; + final states = ['CA', 'NY', 'TX', 'FL', 'IL', 'WA']; + final zipCodes = List.generate(90, (i) => 10000 + (i * 1000) + _random.nextInt(999)); + + final streetNumber = streetNumbers[_random.nextInt(streetNumbers.length)]; + final streetName = streetNames[_random.nextInt(streetNames.length)]; + final city = cities[_random.nextInt(cities.length)]; + final state = states[_random.nextInt(states.length)]; + final zipCode = zipCodes[_random.nextInt(zipCodes.length)]; + + return '$streetNumber $streetName, $city, $state $zipCode'; + } + + static String randomDate() { + final now = DateTime.now(); + final randomDays = _random.nextInt(1000) - 500; + final date = now.add(Duration(days: randomDays)); + + return date.toIso8601String().split('T')[0]; + } + + static String randomDateTime() { + final now = DateTime.now(); + final randomDays = _random.nextInt(100) - 50; + final randomHours = _random.nextInt(24); + final randomMinutes = _random.nextInt(60); + final randomSeconds = _random.nextInt(60); + + final dateTime = now.add(Duration( + days: randomDays, + hours: randomHours, + minutes: randomMinutes, + seconds: randomSeconds, + )); + + return dateTime.toIso8601String(); + } + + static String randomBoolean() { + return _random.nextBool().toString(); + } + + static String randomNumber({int min = 0, int max = 1000}) { + return (_random.nextInt(max - min) + min).toString(); + } + + static String randomJson() { + return '{"id": ${randomId()}, "name": "${randomName()}", "email": "${randomEmail()}", "active": ${randomBoolean()}}'; + } + + static String processFakeDataTag(String tag) { + switch (tag.toLowerCase()) { + case 'randomusername': + return randomUsername(); + case 'randomemail': + return randomEmail(); + case 'randomid': + return randomId(); + case 'randomuuid': + return randomUuid(); + case 'randomname': + return randomName(); + case 'randomphone': + return randomPhone(); + case 'randomaddress': + return randomAddress(); + case 'randomdate': + return randomDate(); + case 'randomdatetime': + return randomDateTime(); + case 'randomboolean': + return randomBoolean(); + case 'randomnumber': + return randomNumber(); + case 'randomjson': + return randomJson(); + default: + return '{{$tag}}'; + } + } +}