├── .github └── workflows │ └── workflow1.yml ├── README.md ├── analysis_options.yaml ├── firebase.json ├── lib ├── authentication │ ├── authentication.dart │ └── authentication_service.dart ├── dto │ ├── admin_dashboard_cache_model.dart │ ├── constant.dart │ ├── issue.dart │ ├── issue_model.dart │ ├── issue_model.g.dart │ ├── pull_model.dart │ ├── pull_model.g.dart │ ├── repo_model.dart │ ├── repo_model.g.dart │ └── table_data.dart ├── main.dart ├── page │ ├── bottom_sheet.dart │ ├── drawer_widget.dart │ ├── issue_details_mobile.dart │ ├── issue_details_page.dart │ ├── issue_details_web.dart │ ├── login_page.dart │ ├── mobile_repo_details.dart │ ├── repo_details_page.dart │ ├── repo_list_element.dart │ ├── repo_page.dart │ ├── web_repo_details.dart │ └── welcome_page.dart ├── provider │ └── provider_list.dart ├── route │ └── routes.dart ├── service │ ├── basic_service.dart │ ├── dashboard_services.dart │ ├── fire_base.dart │ ├── github_service.dart │ ├── issue_service.dart │ ├── pull_request_service.dart │ └── table_data_service.dart └── view_util.dart ├── pubspec.yaml └── web ├── favicon.png ├── icons ├── Icon-192.png ├── Icon-512.png ├── Icon-maskable-192.png └── Icon-maskable-512.png ├── index.html └── manifest.json /.github/workflows/workflow1.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | build_web: 11 | name: Build Flutter (Web) 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: subosito/flutter-action@v1 16 | with: 17 | channel: 'dev' 18 | 19 | - name: flutter upgrade 20 | run: flutter upgrade 21 | - name: get Version 22 | run: flutter --version 23 | - run: flutter pub get 24 | - run: flutter config --enable-web 25 | - name: Make envfile 26 | uses: SpicyPizza/create-envfile@v1.3 27 | with: 28 | DEBUG: false 29 | envkey_my_admin_dashboard_token: ${{ secrets.ADMIN_DASHBOARD_TOKEN }} 30 | envkey_my_admin_dashboard_clientId: ${{ secrets.ADMIN_DASHBOARD_CLIENTID }} 31 | envkey_my_admin_dashboard_clientSecret: ${{ secrets.ADMIN_DASHBOARD_CLIENTSECRET }} 32 | envkey_my_admin_dashboard_redirectUrl: ${{ secrets.ADMIN_DASHBOARD_REDIRECTURL }} 33 | envkey_my_apiKey: ${{ secrets.APIKEY }} 34 | envkey_my_appId: ${{ secrets.APPID }} 35 | envkey_my_messagingSenderId: ${{ secrets.MESSAGING_SENDER_ID }} 36 | envkey_my_projectId: admin-dashboard-b9503 37 | file_name: env_var.env 38 | fail_on_empty: true 39 | - name: build_app 40 | run: flutter build web 41 | - name: Archive Production Artifact 42 | uses: actions/upload-artifact@master 43 | with: 44 | name: web-build 45 | path: build/web 46 | 47 | deploy_web: 48 | name: Deploy Web to Firebase Hosting 49 | needs: build_web 50 | runs-on: ubuntu-latest 51 | env: 52 | admin_dashboard_token: NOHA 53 | steps: 54 | - name: Checkout Repo 55 | uses: actions/checkout@master 56 | - name: Download Artifact 57 | uses: actions/download-artifact@master 58 | with: 59 | name: web-build 60 | path: build/web 61 | 62 | - name: npx firebase-tools 63 | run: npx firebase-tools 64 | 65 | - name: Deploy to Firebase 66 | uses: FirebaseExtended/action-hosting-deploy@v0 67 | with: 68 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 69 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_ADMIN_DASHBOARD_B9503 }}' 70 | adminDashboardToken: '${{ secrets.ADMIN_DASHBOARD_TOKEN}}' 71 | 72 | projectId: admin-dashboard-b9503 73 | channelId: live 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Community Admin Dashboard 2 | 3 | ## The Issue 4 | The Flutter Community provides several packages to the community and pub. dev that all differ in regards to maintenance requirements. 5 | 6 | The Flutter Community admins struggle to keep up with all of the changes, releases, changelogs, and activities on all packages. Two of the biggest challenges include keeping track of releases, as well as the inability to allow maintainers to trigger package releases on pub. dev without giving them super admin access. 7 | 8 | ## The solution 9 | We are looking to implement a Flutter Community Dashboard that assists admins by rounding up and providing the admins with information regarding issues, latest activities on repositories, maintainers, level of access and implementing a trigger to build and deploy to pub.dev, and more. 10 | [GitHub Pages](https://pages.github.com/). 11 | ## More about the project 12 | This project was initially started as a GSoC'22 project with [@abdelrahmanmagdii](https://github.com/abdelrahmanmagdii) as the mentee and [@mhadaily](https://github.com/mhadaily) as the mentor.
13 | This entire project was implemented using flutter for the frontend and Firebase used in the backend. Firebase was used for authentication purposes (as a single sign-on provider) so that it manages access to github and manages access to specific report pages depending on the access level of the user.
Moreover, the github APIs are not suited to create reports reflecting the repositories' variations over time. Therefore, the firebase backend will also be used to run periodic aggregation APIs and store statistical time variant information so that reports could be created that show the KPI (Key Performance Indicator) variations over time. 14 | 15 | ## What has been achieved so far 16 | During the GSoC period, we were able to 17 | 1. Initialize firebase and create a github action to deploy to firebase hosting 18 | 2. Succesfully implement logging in via github 19 | 3. Retrieve and display a list of all the repositories in the flutter community 20 | 4. Create a Repository dashboard that displays statistical data regarding the repository 21 | 5. Create a sortable table format to be able to display a list of issues/PRs based on the criteria chosen by the user 22 | 6. Retrieve and display issues based on criterias chosen by the user 23 | 7. Everything regarding the frontend 24 | 25 | ## What is left 26 | 1. Some data regarding the repository dashboard still uses dummy providers, not real data from the github API 27 | 2. Create an algorithm which decides whether a repository's status is RED, ORANGE or GREEN (Indicating whether or not it requires a maintainer/admins attention) 28 | 3. Implement a trigger to build and deploy to pub.dev 29 | 4. Previously, our application deployed and was hosted succesfully by firebase, but recently an error started occuring to the untouched code where you now face a bug whenever you try to login via Github on only the web version. We are currently still waiting on firebase's documentation to be updated inorder to implement how to login via github on firebase's new version. 30 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | language: 3 | strict-casts: true 4 | strict-inference: true 5 | strict-raw-types: true 6 | 7 | errors: 8 | close_sinks: ignore 9 | missing_required_param: error 10 | missing_return: error 11 | 12 | exclude: 13 | - test/.test_coverage.dart 14 | - lib/generated_plugin_registrant.dart 15 | 16 | linter: 17 | rules: 18 | - always_declare_return_types 19 | - always_require_non_null_named_parameters 20 | - always_use_package_imports 21 | - annotate_overrides 22 | - avoid_bool_literals_in_conditional_expressions 23 | - avoid_catching_errors 24 | - avoid_double_and_int_checks 25 | - avoid_dynamic_calls 26 | - avoid_empty_else 27 | - avoid_equals_and_hash_code_on_mutable_classes 28 | - avoid_escaping_inner_quotes 29 | - avoid_field_initializers_in_const_classes 30 | - avoid_function_literals_in_foreach_calls 31 | - avoid_init_to_null 32 | - avoid_js_rounded_ints 33 | - avoid_null_checks_in_equality_operators 34 | - avoid_positional_boolean_parameters 35 | - avoid_print 36 | - avoid_private_typedef_functions 37 | - avoid_redundant_argument_values 38 | - avoid_relative_lib_imports 39 | - avoid_renaming_method_parameters 40 | - avoid_return_types_on_setters 41 | - avoid_returning_null 42 | - avoid_returning_null_for_future 43 | - avoid_returning_null_for_void 44 | - avoid_returning_this 45 | - avoid_setters_without_getters 46 | - avoid_shadowing_type_parameters 47 | - avoid_single_cascade_in_expression_statements 48 | - avoid_slow_async_io 49 | - avoid_type_to_string 50 | - avoid_types_as_parameter_names 51 | - avoid_unnecessary_containers 52 | - avoid_unused_constructor_parameters 53 | - avoid_void_async 54 | - avoid_web_libraries_in_flutter 55 | - await_only_futures 56 | - camel_case_extensions 57 | - camel_case_types 58 | - cancel_subscriptions 59 | - cascade_invocations 60 | - cast_nullable_to_non_nullable 61 | - comment_references 62 | - conditional_uri_does_not_exist 63 | - constant_identifier_names 64 | - control_flow_in_finally 65 | - curly_braces_in_flow_control_structures 66 | - deprecated_consistency 67 | - directives_ordering 68 | - empty_catches 69 | - empty_constructor_bodies 70 | - empty_statements 71 | - eol_at_end_of_file 72 | - exhaustive_cases 73 | - file_names 74 | - flutter_style_todos 75 | - hash_and_equals 76 | - implementation_imports 77 | - invariant_booleans 78 | - iterable_contains_unrelated_type 79 | - join_return_with_assignment 80 | - leading_newlines_in_multiline_strings 81 | - library_names 82 | - library_prefixes 83 | - library_private_types_in_public_api 84 | - lines_longer_than_80_chars 85 | - list_remove_unrelated_type 86 | - literal_only_boolean_expressions 87 | - missing_whitespace_between_adjacent_strings 88 | - no_adjacent_strings_in_list 89 | - no_default_cases 90 | - no_duplicate_case_values 91 | - no_logic_in_create_state 92 | - no_runtimeType_toString 93 | - non_constant_identifier_names 94 | - noop_primitive_operations 95 | - null_check_on_nullable_type_parameter 96 | - null_closures 97 | - omit_local_variable_types 98 | - one_member_abstracts 99 | - only_throw_errors 100 | - overridden_fields 101 | - package_api_docs 102 | - package_names 103 | - package_prefixed_library_names 104 | - parameter_assignments 105 | - prefer_adjacent_string_concatenation 106 | - prefer_asserts_in_initializer_lists 107 | - prefer_asserts_with_message 108 | - prefer_collection_literals 109 | - prefer_conditional_assignment 110 | - prefer_const_constructors 111 | - prefer_const_constructors_in_immutables 112 | - prefer_const_declarations 113 | - prefer_const_literals_to_create_immutables 114 | - prefer_constructors_over_static_methods 115 | - prefer_contains 116 | - prefer_equal_for_default_values 117 | - prefer_final_fields 118 | - prefer_final_in_for_each 119 | - prefer_final_locals 120 | - prefer_for_elements_to_map_fromIterable 121 | - prefer_function_declarations_over_variables 122 | - prefer_generic_function_type_aliases 123 | - prefer_if_elements_to_conditional_expressions 124 | - prefer_if_null_operators 125 | - prefer_initializing_formals 126 | - prefer_inlined_adds 127 | - prefer_int_literals 128 | - prefer_interpolation_to_compose_strings 129 | - prefer_is_empty 130 | - prefer_is_not_empty 131 | - prefer_is_not_operator 132 | - prefer_iterable_whereType 133 | - prefer_null_aware_method_calls 134 | - prefer_null_aware_operators 135 | - prefer_single_quotes 136 | - prefer_spread_collections 137 | - prefer_typing_uninitialized_variables 138 | - prefer_void_to_null 139 | - provide_deprecation_message 140 | - public_member_api_docs 141 | - recursive_getters 142 | - require_trailing_commas 143 | - secure_pubspec_urls 144 | - sized_box_for_whitespace 145 | - sized_box_shrink_expand 146 | - slash_for_doc_comments 147 | - sort_child_properties_last 148 | - sort_constructors_first 149 | - sort_pub_dependencies 150 | - sort_unnamed_constructors_first 151 | - test_types_in_equals 152 | - throw_in_finally 153 | - tighten_type_of_initializing_formals 154 | - type_annotate_public_apis 155 | - type_init_formals 156 | - unawaited_futures 157 | - unnecessary_await_in_return 158 | - unnecessary_brace_in_string_interps 159 | - unnecessary_const 160 | - unnecessary_constructor_name 161 | - unnecessary_getters_setters 162 | - unnecessary_lambdas 163 | - unnecessary_late 164 | - unnecessary_new 165 | - unnecessary_null_aware_assignments 166 | - unnecessary_null_checks 167 | - unnecessary_null_in_if_null_operators 168 | - unnecessary_nullable_for_final_variable_declarations 169 | - unnecessary_overrides 170 | - unnecessary_parenthesis 171 | - unnecessary_raw_strings 172 | - unnecessary_statements 173 | - unnecessary_string_escapes 174 | - unnecessary_string_interpolations 175 | - unnecessary_this 176 | - unrelated_type_equality_checks 177 | - use_build_context_synchronously 178 | - use_colored_box 179 | - use_decorated_box 180 | - use_enums 181 | - use_full_hex_values_for_flutter_colors 182 | - use_function_type_syntax_for_parameters 183 | - use_is_even_rather_than_modulo 184 | - use_key_in_widget_constructors 185 | - use_late_for_private_fields_and_variables 186 | - use_named_constants 187 | - use_raw_strings 188 | - use_rethrow_when_possible 189 | - use_setters_to_change_properties 190 | - use_string_buffers 191 | - use_super_parameters 192 | - use_to_and_as_if_applicable 193 | - valid_regexps 194 | - void_checks -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build/web", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/authentication/authentication.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/authentication/authentication_service.dart'; 2 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 3 | import 'package:admin_dashboard/main.dart'; 4 | import 'package:firebase_auth/firebase_auth.dart'; 5 | import 'package:firebase_core/firebase_core.dart'; 6 | import 'package:flutter/cupertino.dart'; 7 | import 'package:github_sign_in/github_sign_in.dart'; 8 | 9 | ///Providing Authentication Service via Firebase 10 | class FirebaseAuthenticationService implements AuthenticationService { 11 | ///Constructor that takes firebaseApp as a parameter 12 | FirebaseAuthenticationService(this.firebaseApp); 13 | 14 | ///Instance of firebaseApp 15 | FirebaseApp firebaseApp; 16 | 17 | 18 | //final FirebaseAuth _firebaseAuth = FirebaseAuth.instance; 19 | 20 | @override 21 | Future emailLogin(String email, String password) async { 22 | final firebaseAuth = FirebaseAuth.instanceFor(app: firebaseApp); 23 | UserCredential userCredential; 24 | userCredential = await firebaseAuth.signInWithEmailAndPassword( 25 | email: email, 26 | password: password, 27 | ); 28 | return userCredential.user?.uid; 29 | } 30 | 31 | @override 32 | ///Handles login through Github interface, stores UID in admin_dashboard_cache 33 | Future githubLogin( 34 | BuildContext context, AdminDashboardCache cache,) async { 35 | // Create a new provider 36 | final githubProvider = GithubAuthProvider(); 37 | 38 | //https://firebase.flutter.dev/docs/auth/usage/ 39 | 40 | if(cache.isWeb) { 41 | FirebaseAuth.instance 42 | .authStateChanges() 43 | .listen((User? user) { 44 | }); 45 | await FirebaseAuth.instance.setPersistence(Persistence.NONE); 46 | // Once signed in, return the UserCredential 47 | final userCredential = 48 | await FirebaseAuth.instance.signInWithPopup(githubProvider); 49 | // debugPrint(userCredential.toString()); 50 | return userCredential; 51 | } 52 | else { 53 | //Create a GitHubSignIn instance 54 | try { 55 | debugPrint('Step 1'); 56 | final GitHubSignIn gitHubSignIn; 57 | gitHubSignIn = GitHubSignIn( 58 | clientId: EnvironmentConfig.clientId, 59 | clientSecret: EnvironmentConfig.clientSecret, 60 | redirectUrl: EnvironmentConfig.redirectUrl, 61 | ); 62 | debugPrint('Step 2'); 63 | 64 | // Trigger the sign-in flow 65 | final result = await gitHubSignIn.signIn(context); 66 | debugPrint('Step 3'); 67 | // if the login is valid, 68 | // store uid in AdminDashboardCache and authenticate using firebase 69 | if (result.token != null) { 70 | debugPrint('Step 4'); 71 | cache..uid = gitHubSignIn.clientId 72 | ..loggedIn = true; 73 | // Create a credential from the access token 74 | final githubAuthCredential = 75 | GithubAuthProvider.credential(result.token ?? ''); 76 | UserCredential userCredential; //local variable for debugging 77 | return userCredential = await FirebaseAuth.instance 78 | .signInWithCredential(githubAuthCredential); 79 | // Once signed in, return the UserCredential 80 | } 81 | throw Exception('Error logging in'); 82 | 83 | }catch(e){ 84 | debugPrint(e.toString()); 85 | rethrow; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/authentication/authentication_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | 5 | ///The interface used to define operations that will be provided by 6 | ///the Authentication Service Implementor 7 | abstract class AuthenticationService { 8 | ///Login using an email and password 9 | Future emailLogin(String email, String password); 10 | ///Authenticate login using github's interface 11 | Future githubLogin(BuildContext context, 12 | AdminDashboardCache cache,); 13 | } 14 | -------------------------------------------------------------------------------- /lib/dto/admin_dashboard_cache_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/repo_model.dart'; 2 | import 'package:admin_dashboard/main.dart'; 3 | ///Our application's cache where user ID is stored 4 | class AdminDashboardCache { 5 | ///user's github ID 6 | String? uid = ''; 7 | ///Token used for authentication 8 | String token = EnvironmentConfig.token; 9 | 10 | ///List of repositories in the repo screen 11 | List myTitleList = []; 12 | /// Instance of simpleRepo object which is used to display a list of repos 13 | /// in an organization 14 | SimpleRepo simpleRepo = SimpleRepo(name: ''); 15 | /// boolean value to see if the platform is mobile or web browser 16 | bool isWeb = false; 17 | /// boolean value to see if the user is logged in or not 18 | bool loggedIn = false; 19 | } 20 | -------------------------------------------------------------------------------- /lib/dto/constant.dart: -------------------------------------------------------------------------------- 1 | ///All constants in project 2 | class Constants { 3 | ///Fixed repo name, Will be read from github/firebase in future 4 | static const String repoNameTest = 'plus_plugins'; 5 | ///Issue keyword in our collection's path 6 | static const String issues = 'issues'; 7 | ///Fixed issue number, will be read from github/firebase in future 8 | static const String issueNumber = '908'; 9 | ///Pull keyword in our collection's path 10 | static const String pulls = 'pulls'; 11 | ///Fixed Pull number, will be read from github/firebase in future 12 | static const String pullNumber = '504'; 13 | ///Link to the activity log in our firebase collection 14 | static const String repoLogLink = 15 | 'https://admin-dashboard-b9503-default-rtdb.firebaseio.com/activity_log'; 16 | ///Flutter community organization's name as a constant 17 | static const String flc = 'fluttercommunity'; 18 | ///Repos keyword constant for the github API 19 | static const String repos = 'repos'; 20 | ///Start of github URL for the API 21 | static const String gitApi = 'https://api.github.com'; 22 | ///Orgs keyword for the github API 23 | static const String orgs = 'orgs'; 24 | ///Text for the repo page from the drawer 25 | static const String printRepos = 'Print a list of all repositories'; 26 | ///Text for the login page from the drawer 27 | static const String login = 'login'; 28 | ///Text for the logout page from the drawer 29 | static const String logout = 'logout'; 30 | ///Title for the repo page 31 | static const String repoTitle = 'Repository page'; 32 | ///Firebase Project name 33 | // static const String projectName = 'admin-dashboard'; 34 | static const String projectName = 'MyAdmin2'; 35 | ///Welcome page title 36 | static const String welcomePageTitle = 'Flutter Admin Dashboard'; 37 | ///Activity log URL 38 | static const String siteUrl = 39 | 'https://admin-dashboard-b9503-default-rtdb.firebaseio.com/activity_log'; 40 | ///Flutter community organization's name as a title 41 | static const String flcTitle = 'Flutter Community'; 42 | ///Status of bottomSheet widget 43 | static const String status = 'Status: RED'; 44 | ///Updated at for bottom sheet widget 45 | static const String update = 'Last Updated At: '; 46 | ///URL for the repos API from github 47 | static const String repoGitUrl = 'https://api.github.com/repos'; 48 | /// the /issues keyword to search for issues in a repo from the github API 49 | static const String repoGitIssue = '/issues'; 50 | ///No data keyword for the table if data retrieval fails 51 | static const String noData = 'No Data'; 52 | ///status keyword in the bottom sheet 53 | static const String statusWord = 'Status'; 54 | ///owner keyword in the bottom sheet 55 | static const String owner = 'Owner'; 56 | ///Login page title 57 | static const String loginPage = 'Login Page'; 58 | ///flutter community path for the api 59 | static const String fluttercommunityPath = '/fluttercommunity/'; 60 | ///Status keyword in the bottom sheet 61 | static const String statusKeyword = 'Status'; 62 | ///Owner keyword in the bottom sheet 63 | static const String ownerKeyword = 'Owner'; 64 | ///the word OK 65 | static const String ok = 'OK'; 66 | } 67 | -------------------------------------------------------------------------------- /lib/dto/issue.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/table_data.dart'; 2 | import 'package:admin_dashboard/service/github_service.dart'; 3 | import 'package:admin_dashboard/service/table_data_service.dart'; 4 | 5 | ///Model of issue that is displayed in our application 6 | class SimpleIssue{ 7 | ///Simple Issue constructor 8 | SimpleIssue({required this.title, required this.url, required this.state, 9 | required this.comments, required this.createdAt, 10 | required this.updatedAt,}); 11 | 12 | ///title of issue 13 | final String title; 14 | ///URL of issue 15 | final String url; 16 | ///State of the issue (Open or closed) 17 | final String state; 18 | ///Number of comments the issue has 19 | final int comments; 20 | ///Date the issue was created 21 | final DateTime createdAt; 22 | ///Last time the issue was updated (new comments, change of state, etc) 23 | final DateTime updatedAt; 24 | 25 | @override 26 | String toString() { 27 | return 'SimpleIssue{title: $title, url: $url, state: $state, ' 28 | 'comments: $comments, ' 29 | 'createdAt: ${createdAt.month}/${createdAt.day}/${createdAt.year},' 30 | ' updatedAt: ${updatedAt.month}/${updatedAt.day}/${updatedAt.year} }'; 31 | } 32 | } 33 | 34 | ///List of issues in a repo to be displayed in the table 35 | class SimpleIssueList{ 36 | ///Constructor 37 | SimpleIssueList(this.repoName); 38 | ///Name of the repository 39 | String repoName; 40 | ///List of issues to be displayed in the table 41 | List data = []; 42 | ///method to add issues to the SimpleIssueList 43 | void add(SimpleIssue issue) { 44 | data.add(issue); 45 | } 46 | 47 | @override 48 | String toString() { 49 | return 'SimpleIssueList{data: $data}'; 50 | } 51 | ///List of issues opened within the last six months 52 | Future> getOpenInLastSixMonths() async{ 53 | if(data.isEmpty){ 54 | final service = GithubService(); 55 | data = await service.getAllIssuesOfRepo(repoName); 56 | } 57 | 58 | final result = []; 59 | for(final issue in data){ 60 | if(issue.state != 'open') { 61 | continue; 62 | } 63 | if(issue.createdAt.difference(DateTime.now()). 64 | compareTo(const Duration(days: 180))>0) { 65 | continue; 66 | } 67 | result.add(issue); 68 | } 69 | 70 | return result; 71 | } 72 | } 73 | ///The actual github table provider that fetches data from github 74 | class GithubTableDataProvider extends TableDataProviderInterface { 75 | ///Create a simple issue list where the repoName is set 76 | GithubTableDataProvider(String repoName) : 77 | simpleIssueList = SimpleIssueList(repoName), super(repoName); 78 | ///Object that contains the list of issues that are displayed in the 79 | ///issue details page for a specific repo 80 | SimpleIssueList simpleIssueList; 81 | @override 82 | Future getOpenInLastSixMonths() async{ 83 | final data = await simpleIssueList.getOpenInLastSixMonths(); 84 | final result = TableData(); 85 | for(final issue in data){ 86 | result.add(TableDataRow().addValue('Title', issue.title) 87 | .addValue('Created', ' ${issue.createdAt.month}/' 88 | '${issue.createdAt.day}/${issue.createdAt.year},',) 89 | .addValue('Last Update', ' ${issue.updatedAt.month}/' 90 | '${issue.updatedAt.day}/${issue.updatedAt.year},',) 91 | .addValue('Comments', issue.comments.toString()), 92 | ); 93 | } 94 | return result; 95 | } 96 | 97 | @override 98 | Future getSecondTable() async{ 99 | return TableData().add(TableDataRow().addValue('Name', 'Bug fix'). 100 | addValue('Open', '3 days'),) 101 | .add(TableDataRow().addValue('Name', 'Enhancement'). 102 | addValue('Open', '7 days'),) 103 | .add(TableDataRow().addValue('Name', 'Feature'). 104 | addValue('Open', '1 month'),) 105 | .add(TableDataRow().addValue('Name', 'Help wanted'). 106 | addValue('Open', '4 months'),); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/dto/issue_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'issue_model.g.dart'; 4 | 5 | @JsonSerializable() 6 | 7 | ///Model of github issues 8 | class Issue { 9 | 10 | ///Model of github issues 11 | Issue({ 12 | required this.issueID, 13 | required this.state, 14 | required this.title, 15 | required this.loggedAt, 16 | required this.closedAt, 17 | required this.closedBy, 18 | required this.commentsNumber, 19 | required this.repository, 20 | }); 21 | ///Conversion of JSON into issue object 22 | factory Issue.fromJson(Map json) => _$IssueFromJson(json); 23 | ///ID of issue retrieved from github API 24 | final int issueID; 25 | ///state of issue 26 | final String state; 27 | ///title of issue 28 | final String title; 29 | ///time issue was logged 30 | final DateTime loggedAt; 31 | ///time issue was closed, null if still open 32 | final DateTime? closedAt; 33 | ///username of user who closed issue, null if still open 34 | final String? closedBy; 35 | ///number of comments in issue, 0 if none 36 | final int? commentsNumber; 37 | ///repo issue belongs to 38 | final String repository; 39 | 40 | ///conversion of issues into JSON 41 | Map toJson() => _$IssueToJson(this); 42 | } 43 | ///Array of labels for an issue 44 | class Label { 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /lib/dto/issue_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'issue_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Issue _$IssueFromJson(Map json) => Issue( 10 | issueID: json['issueID'] as int, 11 | state: json['state'] as String, 12 | title: json['title'] as String, 13 | loggedAt: DateTime.parse(json['loggedAt'] as String), 14 | closedAt: json['closedAt'] == null 15 | ? null 16 | : DateTime.parse(json['closedAt'] as String), 17 | closedBy: json['closedBy'] as String?, 18 | commentsNumber: json['commentsNumber'] as int?, 19 | repository: json['repository'] as String, 20 | ); 21 | 22 | Map _$IssueToJson(Issue instance) => { 23 | 'issueID': instance.issueID, 24 | 'state': instance.state, 25 | 'title': instance.title, 26 | 'loggedAt': instance.loggedAt.toIso8601String(), 27 | 'closedAt': instance.closedAt?.toIso8601String(), 28 | 'closedBy': instance.closedBy, 29 | 'commentsNumber': instance.commentsNumber, 30 | 'repository': instance.repository, 31 | }; 32 | -------------------------------------------------------------------------------- /lib/dto/pull_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'pull_model.g.dart'; 4 | 5 | @JsonSerializable() 6 | ///model of github PR 7 | class Pull { 8 | ///Model of PR 9 | Pull({ 10 | required this.userId, 11 | required this.state, 12 | required this.title, 13 | required this.loggedAt, 14 | required this.closedAt, 15 | required this.closedBy, 16 | required this.commentsNumber, 17 | required this.repository, 18 | }); 19 | ///Conversion of JSON into PR object 20 | factory Pull.fromJson(Map json) => _$PullFromJson(json); 21 | ///user that created PR's ID 22 | final int userId; 23 | ///state of PR 24 | final String state; 25 | ///title of PR 26 | final String title; 27 | ///time PR was logged 28 | final DateTime loggedAt; 29 | ///time PR was closed 30 | final DateTime? closedAt; 31 | ///user who closed PR 32 | final String? closedBy; 33 | ///number of comments in PR 34 | final int? commentsNumber; 35 | ///repository in which PR resides 36 | final String repository; 37 | 38 | ///conversion of PR into JSON 39 | Map toJson() => _$PullToJson(this); 40 | } 41 | -------------------------------------------------------------------------------- /lib/dto/pull_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'pull_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Pull _$PullFromJson(Map json) => Pull( 10 | userId: json['userId'] as int, 11 | state: json['state'] as String, 12 | title: json['title'] as String, 13 | loggedAt: DateTime.parse(json['loggedAt'] as String), 14 | closedAt: json['closedAt'] == null 15 | ? null 16 | : DateTime.parse(json['closedAt'] as String), 17 | closedBy: json['closedBy'] as String?, 18 | commentsNumber: json['commentsNumber'] as int?, 19 | repository: json['repository'] as String, 20 | ); 21 | 22 | Map _$PullToJson(Pull instance) => { 23 | 'userId': instance.userId, 24 | 'state': instance.state, 25 | 'title': instance.title, 26 | 'loggedAt': instance.loggedAt.toIso8601String(), 27 | 'closedAt': instance.closedAt?.toIso8601String(), 28 | 'closedBy': instance.closedBy, 29 | 'commentsNumber': instance.commentsNumber, 30 | 'repository': instance.repository, 31 | }; 32 | -------------------------------------------------------------------------------- /lib/dto/repo_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'repo_model.g.dart'; 4 | @JsonSerializable() 5 | 6 | ///Model of github issues 7 | class Repo { 8 | 9 | ///Model of github repos 10 | Repo({ 11 | required this.name, 12 | required this.openIssuesCount, 13 | required this.watchersCount, 14 | required this.stargazersCount, 15 | required this.language, 16 | required this.url, 17 | }); 18 | ///Conversion of JSON into repo object 19 | factory Repo.fromJson(Map json) => _$RepoFromJson(json); 20 | ///name of repository 21 | final String name; 22 | ///number of open issues in repo 23 | final int openIssuesCount; 24 | ///number of watchers in repo 25 | final int watchersCount; 26 | ///number of star gazers in repo 27 | final int stargazersCount; 28 | ///repo's main programming language 29 | final String language; 30 | ///URL of repo 31 | final String url; 32 | 33 | ///conversion of repos into JSON 34 | Map toJson() => _$RepoToJson(this); 35 | } 36 | ///Class for the repo objects displayed in the repo page 37 | class SimpleRepo extends Comparable{ 38 | ///constructor 39 | SimpleRepo({required this.name}); 40 | ///name of repository 41 | final String name; 42 | 43 | @override 44 | int compareTo(dynamic other) { 45 | if(other is! SimpleRepo) { 46 | return 0; 47 | } 48 | final otherRepo = other; 49 | final myName = name.toLowerCase().replaceAll(RegExp('[-_]'), ' '); 50 | final otherName = 51 | otherRepo.name.toLowerCase().replaceAll(RegExp('[-_]'), ' '); 52 | return myName.compareTo(otherName); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/dto/repo_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'repo_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Repo _$RepoFromJson(Map json) => Repo( 10 | name: json['name'] as String, 11 | openIssuesCount: json['openIssuesCount'] as int, 12 | watchersCount: json['watchersCount'] as int, 13 | stargazersCount: json['stargazersCount'] as int, 14 | language: json['language'] as String, 15 | url: json['url'] as String, 16 | ); 17 | 18 | Map _$RepoToJson(Repo instance) => { 19 | 'name': instance.name, 20 | 'openIssuesCount': instance.openIssuesCount, 21 | 'watchersCount': instance.watchersCount, 22 | 'stargazersCount': instance.stargazersCount, 23 | 'language': instance.language, 24 | 'url': instance.url, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/dto/table_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/constant.dart'; 2 | import 'package:admin_dashboard/page/issue_details_mobile.dart'; 3 | import 'package:admin_dashboard/page/issue_details_web.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | ///Class that handles everything concerning the data in the table 7 | class TableData { 8 | 9 | ///List of TableData elements in the rows 10 | List rowList = []; 11 | 12 | Map flags = {}; 13 | ///Sorts the rows in ascending order 14 | void sort(String columnName){ 15 | rowList.sort((TableDataRow a, TableDataRow b) => 16 | (a.elements[columnName]??'').compareTo(b.elements[columnName]??''),); 17 | } 18 | ///Sorts the rows in descending order 19 | void reverseSort(String columnName){ 20 | rowList.sort((TableDataRow a, TableDataRow b) => 21 | (b.elements[columnName]??'').compareTo(a.elements[columnName]??''),); 22 | } 23 | ///Adds new entry to table 24 | TableData add(TableDataRow row){ 25 | if(flags.isEmpty){ 26 | for(final title in row.elements.keys){ 27 | flags.update(title, (value) => true, ifAbsent: () => true); 28 | } 29 | } 30 | rowList.add(row); 31 | return this; 32 | } 33 | 34 | ///Returns a widget that is scrollable and that also uses the SilverGrid to 35 | ///return the table 36 | CustomScrollView getCustomScrollViewWide(WideIssueWidgetState parent){ 37 | var length = 1; 38 | final childrenList = []; 39 | if(rowList.isEmpty){ 40 | childrenList.add(const Text(Constants.noData, 41 | style: TextStyle(color: Colors.white),),); 42 | } else{ 43 | length = rowList[0].elements.keys.length; 44 | for(final title in rowList[0].elements.keys){ 45 | childrenList.add( 46 | TextButton( 47 | style: ButtonStyle( 48 | foregroundColor: MaterialStateProperty.all(Colors.blue), 49 | ), 50 | onPressed: () { 51 | final flag = flags[title]??true; 52 | if(flag) { 53 | parent.sort(title); 54 | }else{ 55 | parent.reverseSort(title); 56 | } 57 | flags[title] = !flag; 58 | }, 59 | child: Text(title, 60 | style: const TextStyle(fontStyle: FontStyle.italic, 61 | color: Colors.lightBlueAccent,), 62 | ), 63 | ), 64 | 65 | 66 | ); 67 | 68 | 69 | } 70 | for(final row in rowList){ 71 | for(final value in row.elements.values){ 72 | childrenList.add(Container( 73 | alignment: Alignment.center, 74 | child: Text(value.length>20?'${value.substring(0,20)}...':value, 75 | style: const TextStyle(fontStyle: FontStyle.italic, 76 | color: Colors.white,), 77 | ),),); 78 | } 79 | 80 | } 81 | } 82 | 83 | final result = CustomScrollView(slivers: [ 84 | SliverGrid.count( 85 | childAspectRatio: 4, 86 | crossAxisCount: length, 87 | children: childrenList,), 88 | 89 | ], 90 | ); 91 | return result; 92 | } 93 | ///Returns a widget that is scrollable and that also uses the SilverGrid to 94 | ///return the table for narrow devices 95 | CustomScrollView getCustomScrollViewNarrow(NarrowIssueWidgetState parent){ 96 | var length = 1; 97 | final childrenList = []; 98 | if(rowList.isEmpty){ 99 | childrenList.add(const Text(Constants.noData, 100 | style: TextStyle(color: Colors.white),),); 101 | } else{ 102 | length = rowList[0].elements.keys.length; 103 | for(final title in rowList[0].elements.keys){ 104 | childrenList.add( 105 | TextButton( 106 | style: ButtonStyle( 107 | foregroundColor: MaterialStateProperty.all(Colors.blue), 108 | ), 109 | onPressed: () { 110 | final flag = flags[title]??true; 111 | if(flag) { 112 | parent.sort(title); 113 | }else{ 114 | parent.reverseSort(title); 115 | } 116 | flags[title] = !flag; 117 | }, 118 | child: Text(title, 119 | style: const TextStyle(fontStyle: FontStyle.italic, 120 | color: Colors.lightBlueAccent,), 121 | ), 122 | ), 123 | 124 | 125 | ); 126 | 127 | 128 | } 129 | for(final row in rowList){ 130 | for(final value in row.elements.values){ 131 | childrenList.add(Container( 132 | alignment: Alignment.center, 133 | child: Text(value.length>20?'${value.substring(0,20)}...':value, 134 | style: const TextStyle(fontStyle: FontStyle.italic, 135 | color: Colors.white,), 136 | ),),); 137 | } 138 | 139 | } 140 | } 141 | 142 | final result = CustomScrollView(slivers: [ 143 | SliverGrid.count( 144 | childAspectRatio: 4, 145 | crossAxisCount: length, 146 | children: childrenList,), 147 | 148 | ], 149 | ); 150 | return result; 151 | } 152 | 153 | } 154 | 155 | ///Adds value to the row of a table 156 | class TableDataRow{ 157 | ///elements in the table 158 | Map elements = {}; 159 | ///Adds values to the table row 160 | TableDataRow addValue(String key, String value){ 161 | //this doesn't write if the key does already exists so use update 162 | //with ifAbsentinstead 163 | elements.update(key, (value) => value, ifAbsent: () => value); 164 | return this; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/constant.dart'; 2 | import 'package:admin_dashboard/provider/provider_list.dart'; 3 | import 'package:admin_dashboard/route/routes.dart'; 4 | import 'package:firebase_core/firebase_core.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 7 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 8 | import 'package:web_browser_detect/web_browser_detect.dart'; 9 | 10 | 11 | Future main() async { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | await dotenv.load(fileName: 'env_var.env'); 14 | 15 | final browser = Browser.detectOrNull(); 16 | FirebaseApp firebaseApp; 17 | 18 | if(browser == null) { 19 | firebaseApp = await Firebase.initializeApp( 20 | name: Constants.projectName, 21 | options: FirebaseOptions( 22 | apiKey: EnvironmentConfig.apiKey, 23 | appId: EnvironmentConfig.appId, 24 | messagingSenderId: EnvironmentConfig.messagingSenderId, 25 | projectId: EnvironmentConfig.projectId, 26 | ), 27 | ); 28 | } else { 29 | firebaseApp = await Firebase.initializeApp( 30 | options: FirebaseOptions( 31 | authDomain: 'admin-dashboard.firebaseapp.com', 32 | apiKey: EnvironmentConfig.apiKey, 33 | appId: EnvironmentConfig.appId, 34 | messagingSenderId: EnvironmentConfig.messagingSenderId, 35 | projectId: EnvironmentConfig.projectId, 36 | ), 37 | ); 38 | } 39 | 40 | runApp( 41 | ProviderScope( 42 | child: MyAppRoutes( firebaseApp: firebaseApp,), 43 | ), 44 | ); 45 | } 46 | 47 | /// MyAppRoutes is the default page run at start up, it handles navigation 48 | /// to the homepage and calls the class generateRoute when navigating 49 | class MyAppRoutes extends ConsumerWidget { 50 | ///MyAppRoutes constructor 51 | const MyAppRoutes( {required this.firebaseApp, Key? key}) : 52 | super(key: key); 53 | 54 | ///instance of firebaseApp 55 | final FirebaseApp firebaseApp; 56 | 57 | @override 58 | Widget build(BuildContext context, WidgetRef ref) { 59 | final cache = ref.read(cacheProvider); 60 | //to detect if web 61 | final browser = Browser.detectOrNull(); 62 | if(browser == null){ 63 | cache.isWeb = false; 64 | } 65 | else{ 66 | cache.isWeb = true; 67 | } 68 | 69 | return MaterialApp( 70 | onGenerateTitle: (context) => Constants.welcomePageTitle, 71 | initialRoute: RouteGenerator.loginPage, 72 | onGenerateRoute: //RouteGenerator.generateRoute, 73 | (settings) { 74 | return RouteGenerator.generateRoute(settings, //cache, 75 | firebaseApp, ); 76 | }, 77 | debugShowCheckedModeBanner: false, 78 | ); 79 | } 80 | } 81 | 82 | ///environment variables 83 | class EnvironmentConfig { 84 | ///logged in client Token which is stored as a environment variable 85 | static final token = dotenv.get('MY_ADMIN_DASHBOARD_TOKEN', fallback: 86 | 'no .env',); 87 | ///logged in client ID which is stored as a environment variable 88 | static final clientId = 89 | dotenv.get('MY_ADMIN_DASHBOARD_CLIENTID', fallback: 'no .env'); 90 | ///logged in client Secret which is stored as a environment variable 91 | static final clientSecret = 92 | dotenv.get('MY_ADMIN_DASHBOARD_CLIENTSECRET', fallback: 'no .env'); 93 | ///The environment variable containing the redirect URL of GitHub 94 | static final redirectUrl = 95 | dotenv.get('MY_ADMIN_DASHBOARD_REDIRECTURL', fallback: 'no .env'); 96 | ///our firebase's API key as an environment variable 97 | static final apiKey = dotenv.get('MY_APIKEY', fallback: 'no .env'); 98 | ///our android appID from the firebase console as an environment variable 99 | static final appId = dotenv.get('MY_APPID', fallback: 'no .env'); 100 | ///our messagingSenderID from the firebase console as an environment variable 101 | static final messagingSenderId = 102 | dotenv.get('MY_MESSAGINGSENDERID', fallback: 'no .env'); 103 | ///Our projectID from the firebase console as an environment variable 104 | static final projectId = dotenv.get('MY_PROJECTID', fallback: 'no .env'); 105 | } 106 | -------------------------------------------------------------------------------- /lib/page/bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 2 | import 'package:admin_dashboard/dto/constant.dart'; 3 | import 'package:admin_dashboard/dto/repo_model.dart'; 4 | import 'package:firebase_core/firebase_core.dart'; 5 | import 'package:flutter/material.dart'; 6 | ///Bottom sheet where the repo owner, status and name are displayed 7 | class BottomSheet extends StatelessWidget{ 8 | ///Constructor 9 | const BottomSheet( this.cache, this.firebaseApp, this.simpleRepo, 10 | {Key? key,}):super(key: key); 11 | 12 | ///Instance of our admin dashboard's cache 13 | final AdminDashboardCache cache; 14 | 15 | ///instance of firebaseApp 16 | final FirebaseApp firebaseApp; 17 | 18 | ///Instance of simpleRepo object used in the repository page 19 | final SimpleRepo simpleRepo; 20 | @override 21 | Widget build(BuildContext context) { 22 | return Container( 23 | constraints: const BoxConstraints( 24 | maxHeight: 30, 25 | maxWidth: 5000, 26 | minWidth: 150, 27 | minHeight: 20, 28 | ), 29 | child: BottomAppBar( 30 | color: Colors.black12, 31 | child: Row( 32 | children: [ 33 | const Spacer(), 34 | Container( 35 | alignment: Alignment.center, 36 | child: Text(cache.simpleRepo.name), 37 | ), 38 | const Spacer(), 39 | Container( 40 | alignment: Alignment.center, 41 | child: const Text(Constants.statusWord), 42 | ), 43 | const Spacer(), 44 | Container( 45 | alignment: Alignment.center, 46 | child: const Text(Constants.owner), 47 | ), 48 | const Spacer(), 49 | ], 50 | ), 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/page/drawer_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/authentication/authentication.dart'; 2 | import 'package:admin_dashboard/dto/constant.dart'; 3 | import 'package:admin_dashboard/route/routes.dart'; 4 | import 'package:firebase_core/firebase_core.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | ///Drawer widget that is used for navigation throughout the app 8 | class DrawerWidget extends StatelessWidget{ 9 | ///Constructor for the drawer widget 10 | 11 | const DrawerWidget( 12 | this.firebaseApp, {Key? key,}) : super(key: key); 13 | ///Instance of firebaseApp 14 | final FirebaseApp firebaseApp; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | 19 | return buildLoggedIn(context); 20 | } 21 | ///Build for items in the drawer widget 22 | Widget buildMenuItem({ 23 | required String text, 24 | required IconData icon, 25 | VoidCallback? onClicked, 26 | }) { 27 | const color = Colors.white; 28 | 29 | return ListTile( 30 | leading: Icon(icon, color: color), 31 | title: Text(text, style: const TextStyle(color: color)), 32 | onTap: onClicked, 33 | ); 34 | } 35 | ///the login method 36 | Future login(BuildContext context) async { 37 | FirebaseAuthenticationService(firebaseApp); 38 | } 39 | 40 | ///Build drawer widget for when a user isn't logged in 41 | Widget buildNotLoggedIn(BuildContext context) { 42 | return Drawer( 43 | child: Material( 44 | color: Colors.deepPurple, 45 | child: ListView( 46 | children: [ 47 | const SizedBox(height: 48), 48 | buildMenuItem( 49 | text: Constants.login, 50 | icon: Icons.play_arrow, 51 | onClicked: () => {login(context)}, 52 | ), 53 | // 54 | buildMenuItem(text: Constants.printRepos, 55 | icon: Icons.play_arrow, 56 | onClicked: () =>{ 57 | Navigator.of(context).pushReplacementNamed( 58 | RouteGenerator.repoPage,) 59 | }, 60 | ), 61 | buildMenuItem(text: 'placeholder', 62 | icon: Icons.abc, 63 | onClicked: () => { 64 | }, 65 | ), 66 | ], 67 | ), 68 | ), 69 | ); 70 | 71 | } 72 | ///Drawer widget if the user is logged in 73 | Widget buildLoggedIn(BuildContext context) { 74 | return Drawer( 75 | child: Material( 76 | color: Colors.deepPurple, 77 | child: ListView( 78 | children: [ 79 | const SizedBox(height: 48), 80 | // 81 | buildMenuItem(text: Constants.printRepos, 82 | icon: Icons.play_arrow, 83 | onClicked: () =>{ 84 | Navigator.of(context).pushReplacementNamed( 85 | RouteGenerator.repoPage,) 86 | }, 87 | ), 88 | ], 89 | ), 90 | ), 91 | ); 92 | 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/page/issue_details_mobile.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 2 | import 'package:admin_dashboard/dto/constant.dart'; 3 | import 'package:admin_dashboard/dto/issue.dart'; 4 | import 'package:admin_dashboard/dto/repo_model.dart'; 5 | import 'package:admin_dashboard/dto/table_data.dart'; 6 | import 'package:admin_dashboard/page/drawer_widget.dart'; 7 | import 'package:admin_dashboard/provider/provider_list.dart'; 8 | import 'package:admin_dashboard/service/table_data_service.dart'; 9 | import 'package:firebase_core/firebase_core.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 12 | 13 | ///Issue page displayed on mobile devices 14 | class NarrowIssuePage extends ConsumerWidget { 15 | ///Constructor 16 | const NarrowIssuePage( this.firebaseApp, this.simpleRepo, this.myWidth, 17 | this.myHeight,); 18 | 19 | ///instance of firebaseApp 20 | final FirebaseApp firebaseApp; 21 | ///instance of simpleRepo that is used to display a list of simple repos 22 | final SimpleRepo simpleRepo; 23 | ///width of the screen 24 | final double myWidth; 25 | ///height of the screen 26 | final double myHeight; 27 | 28 | 29 | @override 30 | Widget build(BuildContext context, WidgetRef ref) { 31 | final cache = ref.read(cacheProvider); 32 | final dataProvider = GithubTableDataProvider(cache.simpleRepo.name); 33 | return NarrowIssueWidget(firebaseApp, simpleRepo, myWidth, 34 | myHeight, cache, dataProvider,); 35 | } 36 | } 37 | ///State for the mobile issue page 38 | class NarrowIssueWidget extends StatefulWidget { 39 | ///Constructor 40 | const NarrowIssueWidget(this.firebaseApp, this.simpleRepo, this.myWidth, 41 | this.myHeight, this.cache, this.tableDataProvider,); 42 | 43 | @override 44 | State createState() { 45 | return NarrowIssueWidgetState( 46 | firebaseApp, simpleRepo, myWidth, myHeight, 47 | cache, tableDataProvider,); 48 | } 49 | ///instance of firebaseApp 50 | final FirebaseApp firebaseApp; 51 | ///instance of simpleRepo 52 | final SimpleRepo simpleRepo; 53 | ///width of the screen 54 | final double myWidth; 55 | ///height of the screen 56 | final double myHeight; 57 | /// Instance of our Admin Dashboard's cache 58 | final AdminDashboardCache cache; 59 | ///Instance of the provider for the tableData 60 | final TableDataProviderInterface tableDataProvider; 61 | } 62 | ///State for the issue page for mobile devices 63 | class NarrowIssueWidgetState extends State { 64 | ///Constructor 65 | NarrowIssueWidgetState( this.firebaseApp, this.simpleRepo, this.myWidth, 66 | this.myHeight, this.cache, this.tableDataProvider,); 67 | ///Instance of firebase App 68 | final FirebaseApp firebaseApp; 69 | ///Instance of simple repo 70 | final SimpleRepo simpleRepo; 71 | ///Width of the screen 72 | final double myWidth; 73 | ///Height of the screen 74 | final double myHeight; 75 | ///Instance of our admin dashboard's cache 76 | final AdminDashboardCache cache; 77 | ///Instance of the provider for the table data 78 | final TableDataProviderInterface tableDataProvider; 79 | ///contains the data of all the issues for the chosen repo 80 | late Future data; 81 | ///subset of the data of all the issues that matches the criteria chosen 82 | late TableData currentData; 83 | @override 84 | void initState() { 85 | super.initState(); 86 | data = tableDataProvider.getOpenInLastSixMonths(); 87 | } 88 | @override 89 | Widget build(BuildContext context) { 90 | final width = MediaQuery.of(context).size.width; 91 | return Scaffold( 92 | backgroundColor: Colors.grey, 93 | drawer: DrawerWidget( 94 | firebaseApp, 95 | ), 96 | appBar: AppBar( 97 | backgroundColor: Colors.deepPurpleAccent, 98 | title: Text( 99 | cache.simpleRepo.name.replaceAll(RegExp('/fluttercommunity/'), ''), 100 | ), 101 | ), 102 | body: 103 | ConstrainedBox( 104 | constraints: BoxConstraints(minWidth: width), 105 | child: FutureBuilder( 106 | future: data, 107 | builder: (context, snapshot) { 108 | if (snapshot.connectionState == ConnectionState.done) { 109 | if (snapshot.hasData) { 110 | currentData = snapshot.data!; 111 | return currentData.getCustomScrollViewNarrow(this); 112 | } else if (snapshot.hasError) { 113 | return Text('${snapshot.error}'); 114 | } 115 | } 116 | return const LinearProgressIndicator(); 117 | }, 118 | ), 119 | ), 120 | 121 | bottomSheet: buildBottomSheet(context, cache, 122 | ), 123 | ); 124 | } 125 | ///The widget for our application's bottom sheet 126 | Widget buildBottomSheet(BuildContext context, AdminDashboardCache cache, 127 | ) { 128 | return Container( 129 | constraints: const BoxConstraints( 130 | maxHeight: 30, 131 | maxWidth: 5000, 132 | minWidth: 150, 133 | minHeight: 20, 134 | ), 135 | child: BottomAppBar( 136 | color: Colors.black12, 137 | child: Row( 138 | children: [ 139 | const Spacer(), 140 | Container( 141 | alignment: Alignment.center, 142 | child: Text( 143 | cache.simpleRepo.name. 144 | replaceAll(RegExp('/fluttercommunity/'),''),), 145 | ), 146 | const Spacer(), 147 | Container( 148 | alignment: Alignment.center, 149 | child: const Text(Constants.statusKeyword), 150 | ), 151 | const Spacer(), 152 | Container( 153 | alignment: Alignment.center, 154 | child: const Text(Constants.ownerKeyword), 155 | ), 156 | const Spacer(), 157 | ], 158 | ), 159 | ), 160 | ); 161 | } 162 | 163 | ///Sort method that sorts the table by ascending 164 | void sort(String title) { 165 | setState(() { 166 | currentData.sort(title); 167 | }); 168 | } 169 | 170 | ///Sort method that sorts the table by descending 171 | void reverseSort(String title) { 172 | setState(() { 173 | currentData.reverseSort(title); 174 | }); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /lib/page/issue_details_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/repo_model.dart'; 2 | import 'package:admin_dashboard/page/issue_details_mobile.dart'; 3 | import 'package:admin_dashboard/page/issue_details_web.dart'; 4 | import 'package:firebase_core/firebase_core.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | ///state of the issue details page 8 | class IssueDetailsPage extends StatefulWidget { 9 | ///Issue page constructor 10 | const IssueDetailsPage({ 11 | required this.firebaseApp, 12 | Key? key, 13 | }) : super(key: key); 14 | 15 | ///instance of firebaseApp 16 | final FirebaseApp firebaseApp; 17 | 18 | @override 19 | State createState() => 20 | _IssueDetailsPageState( firebaseApp, ); 21 | } 22 | 23 | class _IssueDetailsPageState extends State { 24 | _IssueDetailsPageState( this.firebaseApp, ); 25 | 26 | FirebaseApp firebaseApp; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | final myWidth = MediaQuery 31 | .of(context) 32 | .size 33 | .width; 34 | final myHeight = MediaQuery 35 | .of(context) 36 | .size 37 | .height; 38 | var simpleRepo = SimpleRepo(name: ''); 39 | final args = ModalRoute 40 | .of(context)! 41 | .settings 42 | .arguments ?? simpleRepo; 43 | simpleRepo = args as SimpleRepo; 44 | if (myWidth >= 600) { 45 | return buildWide(context, simpleRepo, myWidth, myHeight); 46 | } else { 47 | return buildNarrow(context, simpleRepo, myWidth, myHeight); 48 | } 49 | } 50 | Widget buildNarrow(BuildContext context, SimpleRepo simpleRepo, 51 | double myWidth, double myHeight,) { 52 | return NarrowIssuePage(firebaseApp, simpleRepo, myWidth, myHeight); 53 | } 54 | 55 | Widget buildWide(BuildContext context, SimpleRepo simpleRepo, double myWidth, 56 | double myHeight,) { 57 | return WideIssuePage(firebaseApp, simpleRepo, myWidth, myHeight, ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/page/issue_details_web.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 2 | import 'package:admin_dashboard/dto/constant.dart'; 3 | import 'package:admin_dashboard/dto/issue.dart'; 4 | import 'package:admin_dashboard/dto/repo_model.dart'; 5 | import 'package:admin_dashboard/dto/table_data.dart'; 6 | import 'package:admin_dashboard/page/drawer_widget.dart'; 7 | import 'package:admin_dashboard/provider/provider_list.dart'; 8 | import 'package:admin_dashboard/service/table_data_service.dart'; 9 | import 'package:firebase_core/firebase_core.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 12 | 13 | ///Page displayed when a wide (web or tablet) device is used 14 | class WideIssuePage extends ConsumerWidget { 15 | ///Constructor for Wide Issue Page 16 | const WideIssuePage(this.firebaseApp, this.simpleRepo, this.myWidth, 17 | this.myHeight,); 18 | ///Instance of firebaseApp 19 | final FirebaseApp firebaseApp; 20 | ///Instance of simpleRepo 21 | final SimpleRepo simpleRepo; 22 | ///Width of the screen 23 | final double myWidth; 24 | ///Height of the screen 25 | final double myHeight; 26 | 27 | @override 28 | Widget build(BuildContext context, WidgetRef ref) { 29 | final cache = ref.read(cacheProvider); 30 | final dataProvider = GithubTableDataProvider(cache.simpleRepo.name); 31 | return WideIssueWidget(firebaseApp, simpleRepo, myWidth, 32 | myHeight, cache, dataProvider,); 33 | } 34 | } 35 | 36 | ///Widget for the web/tablet version of our application 37 | class WideIssueWidget extends StatefulWidget { 38 | ///Constructor 39 | const WideIssueWidget(this.firebaseApp, this.simpleRepo, this.myWidth, 40 | this.myHeight, this.cache, this.tableDataProvider,); 41 | @override 42 | State createState() { 43 | return WideIssueWidgetState( 44 | firebaseApp, simpleRepo, myWidth, myHeight, 45 | cache, tableDataProvider,); 46 | } 47 | 48 | ///instance of firebaseApp 49 | final FirebaseApp firebaseApp; 50 | ///instance of simpleRepo 51 | final SimpleRepo simpleRepo; 52 | ///width of the screen 53 | final double myWidth; 54 | ///height of the screen 55 | final double myHeight; 56 | /// Instance of our Admin Dashboard's cache 57 | final AdminDashboardCache cache; 58 | ///Instance of the provider for the tableData 59 | final TableDataProviderInterface tableDataProvider; 60 | } 61 | 62 | 63 | ///State for the wide issue widget 64 | class WideIssueWidgetState extends State { 65 | ///Constructor 66 | WideIssueWidgetState( this.firebaseApp, 67 | this.simpleRepo, 68 | this.myWidth, 69 | this.myHeight, this.cache, this.tableDataProvider,); 70 | 71 | ///Instance of firebase App 72 | final FirebaseApp firebaseApp; 73 | ///Instance of simple repo 74 | final SimpleRepo simpleRepo; 75 | ///Width of the screen 76 | final double myWidth; 77 | ///Height of the screen 78 | final double myHeight; 79 | ///Instance of our admin dashboard's cache 80 | final AdminDashboardCache cache; 81 | ///Instance of the provider for the table data 82 | final TableDataProviderInterface tableDataProvider; 83 | ///contains the data of all the issues for the chosen repo 84 | late Future data; 85 | ///subset of the data of all the issues that matches the criteria chosen 86 | late TableData currentData; 87 | @override 88 | void initState() { 89 | super.initState(); 90 | data = tableDataProvider.getOpenInLastSixMonths(); 91 | } 92 | @override 93 | Widget build(BuildContext context) { 94 | final width = MediaQuery.of(context).size.width; 95 | return Scaffold( 96 | backgroundColor: Colors.grey, 97 | drawer: DrawerWidget( 98 | firebaseApp, 99 | ), 100 | appBar: AppBar( 101 | backgroundColor: Colors.deepPurpleAccent, 102 | title: Text( 103 | cache.simpleRepo.name.replaceAll(RegExp('/fluttercommunity/'), ''), 104 | ), 105 | ), 106 | body: 107 | ConstrainedBox( 108 | constraints: BoxConstraints(minWidth: width), 109 | child: FutureBuilder( 110 | future: data, 111 | builder: (context, snapshot) { 112 | if (snapshot.connectionState == ConnectionState.done) { 113 | if (snapshot.hasData) { 114 | currentData = snapshot.data!; 115 | //return currentData.getDataTable(); 116 | return currentData.getCustomScrollViewWide(this); 117 | } else if (snapshot.hasError) { 118 | return Text('${snapshot.error}'); 119 | } 120 | } 121 | return const LinearProgressIndicator(); 122 | }, 123 | ), 124 | ), 125 | 126 | bottomSheet: buildBottomSheet(context, cache, 127 | ), 128 | ); 129 | } 130 | 131 | ///The widget for our application's bottom sheet 132 | Widget buildBottomSheet(BuildContext context, AdminDashboardCache cache, 133 | ) { 134 | return Container( 135 | constraints: const BoxConstraints( 136 | maxHeight: 30, 137 | maxWidth: 5000, 138 | minWidth: 150, 139 | minHeight: 20, 140 | ), 141 | child: BottomAppBar( 142 | color: Colors.black12, 143 | child: Row( 144 | children: [ 145 | const Spacer(), 146 | Container( 147 | alignment: Alignment.center, 148 | child: 149 | Text(cache.simpleRepo.name 150 | .replaceAll(RegExp('/fluttercommunity/'),''),), 151 | ), 152 | const Spacer(), 153 | Container( 154 | alignment: Alignment.center, 155 | child: const Text(Constants.statusKeyword), 156 | ), 157 | const Spacer(), 158 | Container( 159 | alignment: Alignment.center, 160 | child: const Text(Constants.ownerKeyword), 161 | ), 162 | const Spacer(), 163 | ], 164 | ), 165 | ), 166 | ); 167 | } 168 | 169 | ///Sort method that sorts the table by ascending 170 | void sort(String title) { 171 | setState(() { 172 | currentData.sort(title); 173 | }); 174 | } 175 | 176 | ///Sort method that sorts the table by descending 177 | void reverseSort(String title) { 178 | setState(() { 179 | currentData.reverseSort(title); 180 | }); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /lib/page/login_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/authentication/authentication.dart'; 2 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 3 | import 'package:admin_dashboard/dto/constant.dart'; 4 | import 'package:admin_dashboard/provider/provider_list.dart'; 5 | import 'package:admin_dashboard/route/routes.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 9 | 10 | ///Default page displayed for a user to login to the application 11 | class LoginPage extends ConsumerWidget{ 12 | ///Login page constructor 13 | const LoginPage({ 14 | required this.firebaseApp, 15 | }); 16 | ///Instance of firebase app 17 | final FirebaseApp firebaseApp; 18 | 19 | @override 20 | Widget build(BuildContext context, WidgetRef ref) { 21 | final cache = ref.read(cacheProvider); 22 | return LoginWidget(firebaseApp: firebaseApp, cache: cache,); 23 | } 24 | 25 | } 26 | 27 | ///state of the login page 28 | class LoginWidget extends StatefulWidget { 29 | 30 | 31 | ///login page 32 | const LoginWidget({ 33 | Key? key, 34 | required this.cache, 35 | required this.firebaseApp, 36 | }) : super(key: key); 37 | ///Instance of firebase app 38 | final FirebaseApp firebaseApp; 39 | ///Instance of admin dashboard cache 40 | final AdminDashboardCache cache; 41 | 42 | 43 | @override 44 | State createState() => _LoginWidgetState(cache, firebaseApp); 45 | } 46 | 47 | class _LoginWidgetState extends State { 48 | _LoginWidgetState(this.cache, this.firebaseApp): 49 | firebaseAuthenticationService = 50 | FirebaseAuthenticationService(firebaseApp); 51 | AdminDashboardCache cache; 52 | final FirebaseApp firebaseApp; 53 | FirebaseAuthenticationService firebaseAuthenticationService; 54 | bool isBusy = false; 55 | bool isLoggedIn = false; 56 | bool enableButton = true; 57 | @override 58 | void initState() { 59 | super.initState(); 60 | } 61 | @override 62 | Widget build(BuildContext context) { 63 | 64 | return Scaffold( 65 | appBar: AppBar( 66 | //todo change 67 | title: const Text(Constants.loginPage), 68 | automaticallyImplyLeading: false, 69 | backgroundColor: Colors.deepPurpleAccent, 70 | ), 71 | body: Center( 72 | child: isBusy 73 | ? const CircularProgressIndicator() 74 | : isLoggedIn? const Text(''): 75 | ElevatedButton( 76 | style: ElevatedButton.styleFrom( 77 | padding: const EdgeInsets.all(20), 78 | primary: Colors.deepPurpleAccent, 79 | onPrimary: Colors.white, 80 | textStyle: const TextStyle(fontSize: 20), 81 | ), 82 | onPressed: loginAction, 83 | child: const Text(Constants.login), 84 | 85 | ), 86 | 87 | ), 88 | ); 89 | } 90 | 91 | 92 | loginAction() async { 93 | setState(() { 94 | isBusy = true; 95 | enableButton = false; 96 | }); 97 | final result = await firebaseAuthenticationService.githubLogin( 98 | context, cache,); 99 | result == null ? Navigator.pushReplacementNamed(context, 100 | RouteGenerator.waitingPage,) 101 | : Navigator.pushReplacementNamed(context, 102 | RouteGenerator.welcomePage,); 103 | if(result!=null) { 104 | setState(() { 105 | isBusy = false; 106 | enableButton = true; 107 | if(result==null){ 108 | } 109 | }); 110 | } 111 | } 112 | } 113 | 114 | ///Displays a circular progress indicator while login is completed 115 | class WaitingPage extends StatelessWidget { 116 | 117 | ///login page 118 | const WaitingPage({ 119 | Key? key, 120 | required this.firebaseApp, 121 | }) : super(key: key); 122 | ///Instancce of firebase App 123 | final FirebaseApp firebaseApp; 124 | 125 | @override 126 | Widget build(BuildContext context) { 127 | return const Scaffold( 128 | body: Center(child: CircularProgressIndicator(),), 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/page/mobile_repo_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 2 | import 'package:admin_dashboard/dto/repo_model.dart'; 3 | import 'package:admin_dashboard/page/drawer_widget.dart'; 4 | import 'package:admin_dashboard/provider/provider_list.dart'; 5 | import 'package:admin_dashboard/route/routes.dart'; 6 | import 'package:admin_dashboard/service/dashboard_services.dart'; 7 | import 'package:firebase_core/firebase_core.dart'; 8 | import 'package:fl_chart/fl_chart.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 11 | 12 | ///Repo page that is displayed on mobile 13 | class NarrowRepoPage extends ConsumerWidget { 14 | ///Constructor for narrow page 15 | const NarrowRepoPage( 16 | this.firebaseApp, 17 | this.simpleRepo, 18 | this.myWidth, 19 | this.myHeight, 20 | ); 21 | 22 | ///instance of firebaseApp 23 | final FirebaseApp firebaseApp; 24 | 25 | ///Instance of simple repo 26 | final SimpleRepo simpleRepo; 27 | 28 | ///Width of the screen 29 | final double myWidth; 30 | 31 | ///Height of the screen 32 | final double myHeight; 33 | 34 | @override 35 | Widget build(BuildContext context, WidgetRef ref) { 36 | final cache = ref.read(cacheProvider); 37 | final dashboardProvider = ref.read(myDashboardProvider); 38 | return MobileSummaryDashboard( 39 | cache, 40 | firebaseApp, 41 | dashboardProvider, 42 | myWidth, 43 | myHeight, 44 | ); 45 | } 46 | } 47 | 48 | ///The dashboard for mobile devices 49 | class MobileSummaryDashboard extends StatefulWidget { 50 | ///Constructor for mobile summary page 51 | const MobileSummaryDashboard( 52 | this.cache, 53 | this.firebaseApp, 54 | this.dashboardProvider, 55 | this.myWidth, 56 | this.myHeight, 57 | ); 58 | 59 | ///Instance of admin dashboard cache 60 | final AdminDashboardCache cache; 61 | 62 | ///Instance of firebase app 63 | final FirebaseApp firebaseApp; 64 | 65 | ///Instance of dashboard provider 66 | final DashboardServiceInterface dashboardProvider; 67 | 68 | ///Width of the screen 69 | final double myWidth; 70 | 71 | ///Height of the screen 72 | final double myHeight; 73 | 74 | @override 75 | State createState() { 76 | return _MobileSummaryDashboard( 77 | cache, 78 | firebaseApp, 79 | dashboardProvider, 80 | myWidth, 81 | myHeight, 82 | ); 83 | } 84 | } 85 | 86 | class _MobileSummaryDashboard extends State { 87 | _MobileSummaryDashboard( 88 | this.cache, 89 | this.firebaseApp, 90 | this.dashboardProvider, 91 | this.myWidth, 92 | this.myHeight, 93 | ); 94 | 95 | AdminDashboardCache cache; 96 | FirebaseApp firebaseApp; 97 | DashboardServiceInterface dashboardProvider; 98 | 99 | final double myWidth; 100 | final double myHeight; 101 | 102 | late Future params; 103 | 104 | @override 105 | void initState() { 106 | super.initState(); 107 | params = dashboardProvider.getDashboardParameters(); 108 | } 109 | 110 | @override 111 | Widget build(BuildContext context) { 112 | return Scaffold( 113 | drawer: DrawerWidget( 114 | firebaseApp, 115 | ), 116 | appBar: AppBar( 117 | backgroundColor: Colors.deepPurpleAccent, 118 | ), 119 | body: FutureBuilder( 120 | future: params, 121 | builder: (context, snapshot) { 122 | if (snapshot.connectionState == ConnectionState.done) { 123 | if (snapshot.hasData) { 124 | final dashboardParams = snapshot.data!; 125 | return scrollView(context, dashboardParams); 126 | } else if (snapshot.hasError) { 127 | return Text('${snapshot.error}'); 128 | } 129 | } 130 | return const CircularProgressIndicator(); 131 | }, 132 | ), 133 | ); 134 | } 135 | 136 | Widget scrollView( 137 | BuildContext context, 138 | DashboardParameterList dashboardParams, 139 | ) { 140 | return SingleChildScrollView( 141 | child: Column( 142 | children: [ 143 | Container( 144 | width: double.infinity, 145 | height: myHeight / 10, 146 | color: Colors.white, 147 | child: GestureDetector( 148 | onTap: () => { 149 | Navigator.of(context).pushReplacementNamed( 150 | RouteGenerator.issueDetailPage, 151 | ) 152 | }, 153 | child: Card( 154 | color: Colors.blueAccent, 155 | child: Center( 156 | child: Text(dashboardParams.issueSummary.issuesOldTitle), 157 | ), 158 | ), 159 | ), 160 | ), 161 | Container( 162 | width: double.infinity, 163 | height: myHeight / 10, 164 | color: Colors.white, 165 | child: GestureDetector( 166 | onTap: () => { 167 | Navigator.of(context).pushReplacementNamed( 168 | RouteGenerator.issueDetailPage, 169 | ) 170 | }, 171 | child: Card( 172 | color: Colors.deepPurpleAccent, 173 | child: Center( 174 | child: Text(dashboardParams.issueSummary.issuesHighActivity), 175 | ), 176 | ), 177 | ), 178 | ), 179 | Container( 180 | width: double.infinity, 181 | height: myHeight / 10, 182 | color: Colors.white, 183 | child: GestureDetector( 184 | onTap: () => { 185 | Navigator.of(context).pushReplacementNamed( 186 | RouteGenerator.issueDetailPage, 187 | ) 188 | }, 189 | child: Card( 190 | color: Colors.blueAccent, 191 | child: Center( 192 | child: Text(dashboardParams.issueSummary.issuesLowActivity), 193 | ), 194 | ), 195 | ), 196 | ), 197 | Container( 198 | width: double.infinity, 199 | height: myHeight / 10, 200 | color: Colors.white, 201 | child: GestureDetector( 202 | onTap: () => { 203 | Navigator.of(context).pushReplacementNamed( 204 | RouteGenerator.issueDetailPage, 205 | ) 206 | }, 207 | child: Card( 208 | color: Colors.deepPurpleAccent, 209 | child: Center( 210 | child: Text(dashboardParams.issueSummary.issuesLabeled), 211 | ), 212 | ), 213 | ), 214 | ), 215 | Container( 216 | width: double.infinity, 217 | height: myHeight / 2, 218 | color: Colors.white, 219 | child: GestureDetector( 220 | onTap: () => { 221 | Navigator.of(context).pushReplacementNamed( 222 | RouteGenerator.issueDetailPage, 223 | ) 224 | }, 225 | child: Card( 226 | color: Colors.white, 227 | child: MyLineChart( 228 | cache: cache, 229 | ), 230 | ), 231 | ), 232 | ), 233 | Container( 234 | width: double.infinity, 235 | height: myHeight / 10, 236 | color: Colors.white, 237 | child: GestureDetector( 238 | onTap: () => { 239 | Navigator.of(context).pushReplacementNamed( 240 | RouteGenerator.issueDetailPage, 241 | ) 242 | }, 243 | child: Card( 244 | color: Colors.blueAccent, 245 | child: Center( 246 | child: Center( 247 | child: 248 | Text(dashboardParams.pullRequestSummary.pullsOldTitle), 249 | ), 250 | ), 251 | ), 252 | ), 253 | ), 254 | Container( 255 | width: double.infinity, 256 | height: myHeight / 10, 257 | color: Colors.white, 258 | child: GestureDetector( 259 | onTap: () => { 260 | Navigator.of(context).pushReplacementNamed( 261 | RouteGenerator.issueDetailPage, 262 | ) 263 | }, 264 | child: Card( 265 | color: Colors.deepPurpleAccent, 266 | child: Center( 267 | child: Text( 268 | dashboardParams.pullRequestSummary.pullsHighActivity, 269 | ), 270 | ), 271 | ), 272 | ), 273 | ), 274 | Container( 275 | width: double.infinity, 276 | height: myHeight / 10, 277 | color: Colors.white, 278 | child: GestureDetector( 279 | onTap: () => { 280 | Navigator.of(context).pushReplacementNamed( 281 | RouteGenerator.issueDetailPage, 282 | ) 283 | }, 284 | child: Card( 285 | color: Colors.blueAccent, 286 | child: Center( 287 | child: Text( 288 | dashboardParams.pullRequestSummary.pullsLowActivity, 289 | ), 290 | ), 291 | ), 292 | ), 293 | ), 294 | Container( 295 | width: double.infinity, 296 | height: myHeight / 10, 297 | color: Colors.white, 298 | child: GestureDetector( 299 | onTap: () => { 300 | Navigator.of(context).pushReplacementNamed( 301 | RouteGenerator.issueDetailPage, 302 | ) 303 | }, 304 | child: GestureDetector( 305 | onTap: () => { 306 | Navigator.of(context).pushReplacementNamed( 307 | RouteGenerator.issueDetailPage, 308 | ) 309 | }, 310 | child: Card( 311 | color: Colors.deepPurpleAccent, 312 | child: Center( 313 | child: Text( 314 | dashboardParams.pullRequestSummary.pullsLabeled, 315 | ), 316 | ), 317 | ), 318 | ), 319 | ), 320 | ), 321 | ], 322 | ), 323 | ); 324 | } 325 | } 326 | 327 | ///Class that creates and displays the line chart found in the repo page 328 | class MyLineChart extends StatelessWidget { 329 | ///Constructor 330 | MyLineChart({Key? key, required this.cache}) : super(key: key); 331 | 332 | ///Instance of our admin dashboard cache 333 | final AdminDashboardCache cache; 334 | 335 | ///Data that is used in the chart (hardcoded for now) 336 | final data = {0: 0, 1: 0.75, 2: 3, 3: 1.2, 4: 1, 5: 5}; 337 | 338 | @override 339 | Widget build(BuildContext context) { 340 | return LineChart( 341 | LineChartData( 342 | minX: 0, 343 | maxX: 5, 344 | minY: 0, 345 | maxY: 5, 346 | lineBarsData: [LineChartBarData(spots: createPoints())], 347 | gridData: FlGridData( 348 | show: true, 349 | getDrawingHorizontalLine: (value) { 350 | return FlLine( 351 | color: Colors.black, 352 | strokeWidth: 1, 353 | ); 354 | }, 355 | ), 356 | ), 357 | ); 358 | } 359 | 360 | ///Creates the points used in the line chart 361 | List createPoints() { 362 | ///List of spots to be used in the line chart 363 | final result = []; 364 | for (final x in data.keys) { 365 | result.add(FlSpot(x, data[x] ?? 0)); 366 | } 367 | return result; 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /lib/page/repo_details_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/repo_model.dart'; 2 | import 'package:admin_dashboard/page/mobile_repo_details.dart'; 3 | import 'package:admin_dashboard/page/web_repo_details.dart'; 4 | import 'package:firebase_core/firebase_core.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | ///Page that displays a repository's details 8 | class RepoDetailsPage extends StatefulWidget { 9 | 10 | ///Constructor 11 | const RepoDetailsPage({ 12 | required this.firebaseApp, 13 | Key? key, 14 | }) : super(key: key); 15 | 16 | ///instance of firebaseApp 17 | final FirebaseApp firebaseApp; 18 | 19 | @override 20 | State createState() => 21 | _RepoDetailsPageState( firebaseApp); 22 | } 23 | 24 | class _RepoDetailsPageState extends State { 25 | _RepoDetailsPageState( this.firebaseApp); 26 | 27 | FirebaseApp firebaseApp; 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | final myWidth = MediaQuery.of(context).size.width; 32 | final myHeight = MediaQuery.of(context).size.height; 33 | var simpleRepo = SimpleRepo(name: ''); 34 | final args = ModalRoute.of(context)!.settings.arguments ?? simpleRepo; 35 | simpleRepo = args as SimpleRepo; 36 | if (myWidth >= 600) { 37 | return buildWide(context, simpleRepo, myWidth, myHeight); 38 | } else { 39 | return buildNarrow(context, simpleRepo, myWidth, myHeight); 40 | } 41 | } 42 | 43 | Widget buildNarrow(BuildContext context, SimpleRepo simpleRepo, 44 | double myWidth, double myHeight,) { 45 | return NarrowRepoPage(firebaseApp, simpleRepo, myWidth, myHeight); 46 | } 47 | 48 | Widget buildWide(BuildContext context, SimpleRepo simpleRepo, double myWidth, 49 | double myHeight,) { 50 | return WideRepoPage( firebaseApp, simpleRepo, myWidth, myHeight); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/page/repo_list_element.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 2 | import 'package:admin_dashboard/dto/repo_model.dart'; 3 | import 'package:admin_dashboard/route/routes.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | ///Element that is used in the list of repos, where a list of all repos are 7 | ///displayed 8 | class RepoListElement{ 9 | ///Constructor 10 | RepoListElement(this.simpleRepo, this.width, this.index); 11 | ///Instance of simpleRepo 12 | SimpleRepo simpleRepo; 13 | ///Width of the screen 14 | double width; 15 | ///Index to change color every other list element 16 | int index; 17 | ///Background color for list elements 18 | late Color backgroundColor = 19 | (index.isEven)? Colors.blueAccent:Colors.deepPurpleAccent; 20 | ///Repo list build method 21 | Widget build(BuildContext context, AdminDashboardCache cache){ 22 | return ElevatedButton( 23 | onPressed: () { 24 | cache.simpleRepo = simpleRepo; 25 | Navigator.pushReplacementNamed( 26 | context, 27 | RouteGenerator.repoDetailsPage, 28 | ); 29 | }, 30 | style: ElevatedButton.styleFrom( 31 | primary: backgroundColor, 32 | onPrimary: Colors.black, 33 | maximumSize: Size(width*0.7, 80), 34 | ), child: Text( 35 | ' ${simpleRepo.name.replaceAll(RegExp('/fluttercommunity/'),'')}',),); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/page/repo_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 2 | import 'package:admin_dashboard/dto/constant.dart'; 3 | import 'package:admin_dashboard/dto/repo_model.dart'; 4 | import 'package:admin_dashboard/page/drawer_widget.dart'; 5 | import 'package:admin_dashboard/page/repo_list_element.dart'; 6 | import 'package:admin_dashboard/provider/provider_list.dart'; 7 | import 'package:admin_dashboard/service/fire_base.dart'; 8 | import 'package:firebase_core/firebase_core.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 11 | 12 | ///Repository page where a list of all repos in an org is displayed 13 | class RepoPage extends ConsumerWidget{ 14 | ///Constructor 15 | const RepoPage( {required this.firebaseApp}); 16 | ///Instance of firebase App 17 | final FirebaseApp firebaseApp; 18 | @override 19 | Widget build(BuildContext context, WidgetRef ref) { 20 | final cache = ref.read(cacheProvider); 21 | return RepoPageWidget(firebaseApp: firebaseApp, cache: cache); 22 | } 23 | 24 | } 25 | 26 | ///state of the repo page 27 | class RepoPageWidget extends StatefulWidget { 28 | ///repo page constructor 29 | const RepoPageWidget({ 30 | Key? key, 31 | //required this.fire, 32 | required this.firebaseApp, 33 | required this.cache, 34 | }) : super(key: key); 35 | 36 | ///Instance of firebaseApp 37 | final FirebaseApp firebaseApp; 38 | ///Instance of the admin dashboard cache 39 | final AdminDashboardCache cache; 40 | 41 | @override 42 | State createState() => _RepoPageWidgetState( 43 | firebaseApp,cache,); 44 | } 45 | 46 | class _RepoPageWidgetState extends State { 47 | _RepoPageWidgetState( //this.fire, 48 | this.firebaseApp, this.cache,); 49 | late Future> myTitleList; 50 | 51 | //BasicServiceInterface fire; 52 | FirebaseApp firebaseApp; 53 | final AdminDashboardCache cache; 54 | 55 | @override 56 | void initState() { 57 | super.initState(); 58 | myTitleList = printRepos(context); 59 | } 60 | @override 61 | Widget build(BuildContext context) { 62 | printRepos(context); 63 | return Scaffold( 64 | drawer: DrawerWidget(//fire, 65 | firebaseApp,), 66 | appBar: AppBar( 67 | backgroundColor: Colors.deepPurple, 68 | title: const Text(Constants.repoTitle), 69 | ), 70 | body: Container( 71 | alignment: Alignment.center, 72 | padding: const EdgeInsets.all(8), 73 | child: FutureBuilder>( 74 | future: myTitleList, 75 | builder: (context, snapshot) { 76 | if (snapshot.connectionState == ConnectionState.done) { 77 | if (snapshot.hasData) { 78 | return ListView( 79 | children: _buildList(snapshot.data!.toList()), 80 | ); 81 | } else if (snapshot.hasError) { 82 | return Text('${snapshot.error}'); 83 | } 84 | } 85 | return const CircularProgressIndicator(); 86 | }, 87 | ), 88 | ), 89 | ); 90 | } 91 | Future> printRepos(BuildContext context) async { 92 | if(cache.myTitleList.isNotEmpty) { 93 | return cache.myTitleList; 94 | } 95 | FireBaseService fireBaseService; 96 | fireBaseService = FireBaseService(firebaseApp); 97 | final list = await fireBaseService.getAllRepos( 98 | context, 99 | cache, 100 | ); 101 | cache.myTitleList = list; 102 | return list; 103 | } 104 | 105 | List _buildList(List list) { 106 | list.sort(); 107 | var index = 0; 108 | final result = []; 109 | for (final simpleRepo in list) { 110 | final element = SimpleRepoElement(index,simpleRepo, cache); 111 | index++; 112 | result.add(element); 113 | } 114 | return result; 115 | } 116 | } 117 | 118 | ///Our repository object that is used in the repository page 119 | class SimpleRepoElement extends StatelessWidget { 120 | ///SimpleRepo constructor 121 | const SimpleRepoElement(this.index,this.simpleRepo, this.cache, {Key? key}) 122 | : super(key: key); 123 | ///Our cache that is used throughout the application, the list of retrieved 124 | ///repositories gets stored in the cache 125 | final AdminDashboardCache cache; 126 | ///SimpleRepo object that is used in the repository page 127 | final SimpleRepo simpleRepo; 128 | ///Index used in the repo page to know which repo has been selected by the 129 | ///user 130 | final int index; 131 | 132 | @override 133 | Widget build(BuildContext context) { 134 | final width = MediaQuery.of(context).size.width; 135 | final element = RepoListElement(simpleRepo, width, index); 136 | return element.build(context, cache); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/page/web_repo_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 2 | import 'package:admin_dashboard/dto/constant.dart'; 3 | import 'package:admin_dashboard/dto/repo_model.dart'; 4 | import 'package:admin_dashboard/page/drawer_widget.dart'; 5 | import 'package:admin_dashboard/provider/provider_list.dart'; 6 | import 'package:admin_dashboard/route/routes.dart'; 7 | import 'package:admin_dashboard/service/dashboard_services.dart'; 8 | import 'package:firebase_core/firebase_core.dart'; 9 | import 'package:fl_chart/fl_chart.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 12 | 13 | ///Repo page that is displayed on tablets/web browsers 14 | class WideRepoPage extends ConsumerWidget { 15 | ///Constructor 16 | const WideRepoPage( 17 | this.firebaseApp, 18 | this.simpleRepo, 19 | this.myWidth, 20 | this.myHeight, 21 | ); 22 | 23 | ///instance of firebaseApp 24 | final FirebaseApp firebaseApp; 25 | 26 | ///Instance of simple repo 27 | final SimpleRepo simpleRepo; 28 | 29 | ///Width of screen 30 | final double myWidth; 31 | 32 | ///Height of screen 33 | final double myHeight; 34 | 35 | @override 36 | Widget build(BuildContext context, WidgetRef ref) { 37 | final cache = ref.read(cacheProvider); 38 | final dashboardProvider = ref.read(myDashboardProvider); 39 | return WebSummaryDashboard( 40 | cache, 41 | firebaseApp, 42 | dashboardProvider, 43 | myWidth, 44 | myHeight, 45 | ); 46 | } 47 | } 48 | 49 | ///The repo dashboard for browsers/tablet devices 50 | class WebSummaryDashboard extends StatefulWidget { 51 | ///Constructor 52 | const WebSummaryDashboard( 53 | this.cache, 54 | this.firebaseApp, 55 | this.dashboardProvider, 56 | this.myWidth, 57 | this.myHeight, 58 | ); 59 | 60 | ///Instance of admin dashboard cache 61 | final AdminDashboardCache cache; 62 | 63 | ///Instance of firebase app 64 | final FirebaseApp firebaseApp; 65 | 66 | ///Instance of dashboard provider 67 | final DashboardServiceInterface dashboardProvider; 68 | 69 | ///Screen's width 70 | final double myWidth; 71 | 72 | ///Screen's height 73 | final double myHeight; 74 | 75 | @override 76 | State createState() { 77 | return _WebSummaryDashboard( 78 | cache, 79 | firebaseApp, 80 | dashboardProvider, 81 | myWidth, 82 | myHeight, 83 | ); 84 | } 85 | } 86 | 87 | class _WebSummaryDashboard extends State { 88 | _WebSummaryDashboard( 89 | this.cache, 90 | this.firebaseApp, 91 | this.dashboardProvider, 92 | this.myWidth, 93 | this.myHeight, 94 | ); 95 | 96 | AdminDashboardCache cache; 97 | FirebaseApp firebaseApp; 98 | DashboardServiceInterface dashboardProvider; 99 | 100 | final double myWidth; 101 | final double myHeight; 102 | 103 | late Future params; 104 | 105 | @override 106 | void initState() { 107 | super.initState(); 108 | params = dashboardProvider.getDashboardParameters(); 109 | } 110 | 111 | @override 112 | Widget build(BuildContext context) { 113 | final width = MediaQuery.of(context).size.width; 114 | final height = MediaQuery.of(context).size.height; 115 | 116 | return Scaffold( 117 | backgroundColor: Colors.grey, 118 | drawer: DrawerWidget( 119 | firebaseApp, 120 | ), 121 | appBar: AppBar( 122 | backgroundColor: Colors.deepPurpleAccent, 123 | title: Text( 124 | cache.simpleRepo.name.replaceAll(RegExp('/fluttercommunity/'), ''), 125 | ), 126 | ), 127 | body: FutureBuilder( 128 | future: params, 129 | builder: (context, snapshot) { 130 | if (snapshot.connectionState == ConnectionState.done) { 131 | if (snapshot.hasData) { 132 | final dashboardParams = snapshot.data!; 133 | return Column( 134 | children: [ 135 | firstRow(context, width, height, dashboardParams), 136 | secondRow(context, width, height, dashboardParams), 137 | ], 138 | ); 139 | } else if (snapshot.hasError) { 140 | return Text('${snapshot.error}'); 141 | } 142 | } 143 | return const CircularProgressIndicator(); 144 | }, 145 | ), 146 | bottomSheet: buildBottomSheet(context), 147 | ); 148 | } 149 | 150 | Widget buildBottomSheet(BuildContext context) { 151 | final dateToday = DateTime.now(); 152 | final date = dateToday.toString().substring(0, 10); 153 | return Container( 154 | constraints: const BoxConstraints( 155 | maxHeight: 30, 156 | maxWidth: 5000, 157 | minWidth: 150, 158 | minHeight: 20, 159 | ), 160 | child: BottomAppBar( 161 | color: Colors.black12, 162 | child: Row( 163 | children: [ 164 | const Spacer(), 165 | Container( 166 | alignment: Alignment.center, 167 | child: const Text(Constants.flcTitle), 168 | ), 169 | const Spacer(), 170 | Container( 171 | alignment: Alignment.center, 172 | child: Text('${Constants.update}$date'), 173 | ), 174 | const Spacer(), 175 | Container( 176 | alignment: Alignment.center, 177 | child: const Text( 178 | Constants.status, 179 | style: TextStyle( 180 | backgroundColor: Colors.red, 181 | ), 182 | ), 183 | ), 184 | const Spacer(), 185 | ], 186 | ), 187 | ), 188 | ); 189 | } 190 | 191 | Widget firstColumn( 192 | BuildContext context, 193 | DashboardParameterList dashboardParams, 194 | ) { 195 | return Expanded( 196 | child: Column( 197 | crossAxisAlignment: CrossAxisAlignment.start, 198 | textBaseline: TextBaseline.alphabetic, 199 | children: [ 200 | GestureDetector( 201 | onTap: () => { 202 | Navigator.of(context) 203 | .pushReplacementNamed(RouteGenerator.issueDetailPage) 204 | }, 205 | child: Container( 206 | margin: const EdgeInsets.only( 207 | bottom: 75, 208 | left: 120, 209 | right: 50, 210 | top: 100, 211 | ), 212 | decoration: BoxDecoration( 213 | color: Colors.white, 214 | borderRadius: BorderRadius.circular(18), 215 | ), 216 | padding: const EdgeInsets.all(15), 217 | child: Text(dashboardParams.issueSummary.issuesOldTitle), 218 | ), 219 | ), 220 | GestureDetector( 221 | onTap: () => { 222 | Navigator.of(context).pushReplacementNamed( 223 | RouteGenerator.issueDetailPage, 224 | ) 225 | }, 226 | child: Container( 227 | margin: const EdgeInsets.only(bottom: 75, left: 120), 228 | decoration: BoxDecoration( 229 | color: Colors.white, 230 | borderRadius: BorderRadius.circular(18), 231 | ), 232 | padding: const EdgeInsets.all(15), 233 | child: Text(dashboardParams.issueSummary.issuesHighActivity), 234 | ), 235 | ), 236 | GestureDetector( 237 | onTap: () => { 238 | Navigator.of(context).pushReplacementNamed( 239 | RouteGenerator.issueDetailPage, 240 | ) 241 | }, 242 | child: Container( 243 | margin: const EdgeInsets.only(bottom: 75, left: 120), 244 | decoration: BoxDecoration( 245 | color: Colors.white, 246 | borderRadius: BorderRadius.circular(18), 247 | ), 248 | padding: const EdgeInsets.all(15), 249 | child: Text(dashboardParams.issueSummary.issuesLowActivity), 250 | ), 251 | ), 252 | GestureDetector( 253 | onTap: () => { 254 | Navigator.of(context).pushReplacementNamed( 255 | RouteGenerator.issueDetailPage, 256 | ) 257 | }, 258 | child: Container( 259 | margin: const EdgeInsets.only(left: 120), 260 | decoration: BoxDecoration( 261 | color: Colors.white, 262 | borderRadius: BorderRadius.circular(18), 263 | ), 264 | padding: const EdgeInsets.all(15), 265 | child: Text(dashboardParams.issueSummary.issuesLabeled), 266 | ), 267 | ), 268 | ], 269 | ), 270 | ); 271 | } 272 | 273 | Widget secondColumn(BuildContext context) { 274 | return Expanded( 275 | child: Column( 276 | children: [ 277 | Container( 278 | alignment: Alignment.topCenter, 279 | height: myHeight / 2, 280 | width: myWidth / 2, 281 | margin: const EdgeInsets.only(top: 125), 282 | child: MyLineChart( 283 | cache: cache, 284 | ), 285 | ) 286 | ], 287 | ), 288 | ); 289 | } 290 | 291 | Widget thirdColumn( 292 | BuildContext context, 293 | DashboardParameterList dashboardParams, 294 | ) { 295 | return Expanded( 296 | child: Column( 297 | children: [ 298 | GestureDetector( 299 | onTap: () => { 300 | Navigator.of(context).pushReplacementNamed( 301 | RouteGenerator.issueDetailPage, 302 | ) 303 | }, 304 | child: Container( 305 | margin: const EdgeInsets.only(bottom: 75, top: 100, right: 120), 306 | decoration: BoxDecoration( 307 | color: Colors.white, 308 | borderRadius: BorderRadius.circular(18), 309 | ), 310 | padding: const EdgeInsets.all(15), 311 | child: Text(dashboardParams.pullRequestSummary.pullsOldTitle), 312 | ), 313 | ), 314 | GestureDetector( 315 | onTap: () => { 316 | Navigator.of(context).pushReplacementNamed( 317 | RouteGenerator.issueDetailPage, 318 | ) 319 | }, 320 | child: Container( 321 | margin: const EdgeInsets.only(bottom: 75, right: 120), 322 | decoration: BoxDecoration( 323 | color: Colors.white, 324 | borderRadius: BorderRadius.circular(18), 325 | ), 326 | padding: const EdgeInsets.all(15), 327 | child: Text( 328 | dashboardParams.pullRequestSummary.pullsHighActivity, 329 | ), 330 | ), 331 | ), 332 | GestureDetector( 333 | onTap: () => { 334 | Navigator.of(context).pushReplacementNamed( 335 | RouteGenerator.issueDetailPage, 336 | ) 337 | }, 338 | child: Container( 339 | margin: const EdgeInsets.only(bottom: 75, right: 120), 340 | decoration: BoxDecoration( 341 | color: Colors.white, 342 | borderRadius: BorderRadius.circular(18), 343 | ), 344 | padding: const EdgeInsets.all(15), 345 | child: Text( 346 | dashboardParams.pullRequestSummary.pullsLowActivity, 347 | ), 348 | ), 349 | ), 350 | GestureDetector( 351 | onTap: () => { 352 | Navigator.of(context).pushReplacementNamed( 353 | RouteGenerator.issueDetailPage, 354 | ) 355 | }, 356 | child: Container( 357 | margin: const EdgeInsets.only(right: 120), 358 | decoration: BoxDecoration( 359 | color: Colors.white, 360 | borderRadius: BorderRadius.circular(18), 361 | ), 362 | padding: const EdgeInsets.all(15), 363 | child: Text(dashboardParams.pullRequestSummary.pullsLabeled), 364 | ), 365 | ), 366 | ], 367 | ), 368 | ); 369 | } 370 | 371 | Widget firstRow( 372 | BuildContext context, 373 | double width, 374 | double height, 375 | DashboardParameterList dashboardParams, 376 | ) { 377 | return Expanded( 378 | child: Container( 379 | decoration: BoxDecoration( 380 | color: Colors.white, 381 | borderRadius: BorderRadius.circular(18), 382 | ), 383 | height: double.maxFinite, 384 | child: Row( 385 | children: [ 386 | const Spacer(), 387 | SizedBox( 388 | width: width / 4, 389 | child: GestureDetector( 390 | onTap: () => { 391 | Navigator.of(context).pushReplacementNamed( 392 | RouteGenerator.issueDetailPage, 393 | ) 394 | }, 395 | child: Text( 396 | dashboardParams.issueSummary.totalIssueTitle + 397 | dashboardParams.issueSummary.totalIssue, 398 | ), 399 | ), 400 | ), 401 | SizedBox( 402 | width: width / 4, 403 | child: GestureDetector( 404 | onTap: () => { 405 | Navigator.of(context).pushReplacementNamed( 406 | RouteGenerator.issueDetailPage, 407 | ) 408 | }, 409 | child: Text( 410 | dashboardParams.issueSummary.dormantIssueTitle + 411 | dashboardParams.issueSummary.totalDormantIssue, 412 | ), 413 | ), 414 | ), 415 | SizedBox( 416 | width: width / 4, 417 | child: GestureDetector( 418 | onTap: () => { 419 | Navigator.of(context).pushReplacementNamed( 420 | RouteGenerator.issueDetailPage, 421 | ) 422 | }, 423 | child: Text( 424 | dashboardParams.pullRequestSummary.dormantPullTitle + 425 | dashboardParams.pullRequestSummary.totalClosedPull, 426 | ), 427 | ), 428 | ), 429 | Expanded( 430 | child: GestureDetector( 431 | onTap: () => { 432 | Navigator.of(context).pushReplacementNamed( 433 | RouteGenerator.issueDetailPage, 434 | ) 435 | }, 436 | child: Text( 437 | dashboardParams.pullRequestSummary.totalIPullTitle + 438 | dashboardParams.pullRequestSummary.totalPull, 439 | ), 440 | ), 441 | ), 442 | ], 443 | ), 444 | ), 445 | ); 446 | } 447 | 448 | Widget secondRow( 449 | BuildContext context, 450 | double width, 451 | double height, 452 | DashboardParameterList dashboardParams, 453 | ) { 454 | return SizedBox( 455 | height: (height * 4) / 5, 456 | child: Row( 457 | children: [ 458 | firstColumn(context, dashboardParams), 459 | secondColumn(context), 460 | thirdColumn(context, dashboardParams), 461 | ], 462 | ), 463 | ); 464 | } 465 | } 466 | 467 | ///Class that creates and displays the line chart found in the repo page 468 | 469 | class MyLineChart extends StatelessWidget { 470 | ///Constructor 471 | 472 | MyLineChart({Key? key, required this.cache}) : super(key: key); 473 | 474 | ///Instance of our admin dashboard cache 475 | 476 | final AdminDashboardCache cache; 477 | 478 | ///Data that is used in the chart (hardcoded for now) 479 | 480 | final data = {0: 0, 1: 0.75, 2: 3, 3: 1.2, 4: 1, 5: 5}; 481 | 482 | @override 483 | Widget build(BuildContext context) { 484 | return LineChart( 485 | LineChartData( 486 | minX: 0, 487 | maxX: 5, 488 | minY: 0, 489 | maxY: 5, 490 | lineBarsData: [ 491 | LineChartBarData( 492 | spots: createPoints(), 493 | ) 494 | ], 495 | gridData: FlGridData( 496 | show: true, 497 | getDrawingHorizontalLine: (value) { 498 | return FlLine( 499 | color: Colors.black, 500 | strokeWidth: 1, 501 | ); 502 | }, 503 | ), 504 | ), 505 | ); 506 | } 507 | 508 | ///Creates the points used in the line chart 509 | List createPoints() { 510 | ///List of spots to be used in the line chart 511 | final result = []; 512 | for (final x in data.keys) { 513 | result.add(FlSpot(x, data[x] ?? 0)); 514 | } 515 | return result; 516 | } 517 | } 518 | -------------------------------------------------------------------------------- /lib/page/welcome_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/constant.dart'; 2 | import 'package:admin_dashboard/page/drawer_widget.dart'; 3 | import 'package:firebase_core/firebase_core.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | ///Default page shown in our application 7 | class WelcomePage extends StatefulWidget { 8 | ///welcome page constructor 9 | const WelcomePage({ 10 | Key? key, 11 | //required this.fire, 12 | required this.firebaseApp, 13 | }) : super(key: key); 14 | ///Instance of firebaseApp 15 | final FirebaseApp firebaseApp; 16 | 17 | ///cache where the logged in user's ID is stored 18 | ///instance to handle firebase services 19 | //final BasicServiceInterface fire; 20 | 21 | @override 22 | State createState() => _WelcomePageState(//cache, //fire, 23 | firebaseApp,); 24 | } 25 | 26 | class _WelcomePageState extends State { 27 | _WelcomePageState(//this.cache, //this.fire, 28 | this.firebaseApp,); 29 | FirebaseApp firebaseApp; 30 | //AdminDashboardCache cache; 31 | //BasicServiceInterface fire; 32 | 33 | @override 34 | Widget build(BuildContext context) => Scaffold( 35 | drawer: DrawerWidget(//cache, //fire, 36 | firebaseApp,), 37 | appBar: AppBar( 38 | title: const Text(Constants.welcomePageTitle), 39 | backgroundColor: Colors.deepPurpleAccent, 40 | ), 41 | ); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /lib/provider/provider_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 2 | import 'package:admin_dashboard/service/dashboard_services.dart'; 3 | import 'package:admin_dashboard/service/issue_service.dart'; 4 | import 'package:admin_dashboard/service/pull_request_service.dart'; 5 | import 'package:admin_dashboard/service/table_data_service.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | 8 | ///Provide the cache for the application 9 | ///It will store information such as the userId, 10 | ///and the token used for authentication 11 | final cacheProvider = Provider((ref) => AdminDashboardCache()); 12 | ///Provides the singleton object of the class FireBaseService 13 | ///It helps implements the DI pattern, as the implementor 14 | ///of the Interface BasicServiceInterface 15 | final issueProvider= Provider((ref) => DummyIssueProvider()); 16 | 17 | ///the dummy pull provider, returns dummy data for the repo page 18 | final pullProvider = Provider((ref) => DummyPullProvider()); 19 | 20 | ///Contains the dummy providers for the repo page 21 | final myDashboardProvider = Provider((ref) => 22 | DashboardServiceImplementor(DummyIssueProvider(), 23 | DummyPullProvider(),),); 24 | 25 | ///Dummy table provider for the repo/issues page 26 | final tabledataProvider = Provider((ref) => DummyTableDataProvider('') 27 | ,); 28 | -------------------------------------------------------------------------------- /lib/route/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/page/issue_details_page.dart'; 2 | import 'package:admin_dashboard/page/login_page.dart'; 3 | import 'package:admin_dashboard/page/repo_details_page.dart'; 4 | import 'package:admin_dashboard/page/repo_page.dart'; 5 | import 'package:admin_dashboard/page/welcome_page.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | 9 | ///The route generator that handles navigation throughout the app\ 10 | class RouteGenerator { 11 | RouteGenerator._(); 12 | ///Welcome page path 13 | static const String welcomePage = '/'; 14 | ///repository page path 15 | static const String repoPage = '/repos'; 16 | ///Login page path 17 | static const String loginPage = '/login'; 18 | ///Repo details path 19 | static const String repoDetailsPage = '/repoDetails'; 20 | ///Waiting page path 21 | static const String waitingPage = '/waiting'; 22 | ///Issue details path 23 | static const String issueDetailPage = '/issueDetails'; 24 | 25 | 26 | 27 | 28 | ///decides which page to open depending on the path provided to the navigator 29 | static Route generateRoute(RouteSettings settings, 30 | FirebaseApp firebaseApp, 31 | 32 | ) { 33 | switch (settings.name) { 34 | case welcomePage: 35 | return MaterialPageRoute( 36 | builder: (_) => WelcomePage(//cache: cache, //fire: fire, 37 | firebaseApp: firebaseApp,), 38 | ); 39 | case repoPage: 40 | return MaterialPageRoute( 41 | builder: (_) => RepoPage(//cache: cache, //fire: fire, 42 | firebaseApp: firebaseApp,), 43 | ); 44 | case repoDetailsPage: 45 | return MaterialPageRoute( 46 | builder: (_) => RepoDetailsPage(//cache: cache, 47 | firebaseApp: firebaseApp,) 48 | ,); 49 | case loginPage: 50 | return MaterialPageRoute( 51 | builder: (_) => LoginPage(//cache: cache, 52 | firebaseApp: firebaseApp, 53 | ) 54 | ,); 55 | case waitingPage: 56 | return MaterialPageRoute( 57 | builder: (_) => WaitingPage(//cache: cache, 58 | firebaseApp: firebaseApp, 59 | ) 60 | ,); 61 | case issueDetailPage: 62 | return MaterialPageRoute( 63 | builder: (_) => IssueDetailsPage(//cache: cache, 64 | firebaseApp: firebaseApp, 65 | ) 66 | ,); 67 | default: 68 | throw const FormatException('Route not found'); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/service/basic_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 2 | import 'package:admin_dashboard/dto/issue_model.dart'; 3 | import 'package:admin_dashboard/dto/pull_model.dart'; 4 | import 'package:admin_dashboard/dto/repo_model.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | 7 | ///The interface used to define methods that the GitHub Service Implementor 8 | ///will provide 9 | abstract class BasicServiceInterface { 10 | ///retrieve all issues across org 11 | List getAllIssues(); 12 | ///retrieve all issues in a specific repo 13 | Future> getAllRepoIssues( 14 | String repoName, 15 | BuildContext context, 16 | AdminDashboardCache cache, 17 | ); 18 | ///adds issue to a specific repo as defined in the issue param 19 | Future addIssue(Issue issue); 20 | 21 | ///Retrieve all PRs across organization 22 | List getAllPulls(); 23 | 24 | ///retrieves all PRs in a specific repo 25 | Future> getAllRepoPulls( 26 | String repoName, 27 | BuildContext context, 28 | AdminDashboardCache cache, 29 | ); 30 | ///adds PR to a specific repo as defined in the PR param 31 | Future addPull(Pull pr); 32 | 33 | ///retrieves all repositories belonging to a specific organization 34 | Future> getAllRepos( BuildContext context, 35 | AdminDashboardCache cache, 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /lib/service/dashboard_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/service/issue_service.dart'; 2 | import 'package:admin_dashboard/service/pull_request_service.dart'; 3 | 4 | ///For the repo page, this contains all the data and provides it to the page 5 | ///currently, dummy data is being used 6 | class DashboardParameterList{ 7 | ///Pull request summary contains all the info regarding PRs in the repo 8 | ///dashboard page 9 | PullSummaryInformation pullRequestSummary = PullSummaryInformation(); 10 | ///Issue summary also contains all the info regarding issues in the 11 | ///repo dashboard page 12 | IssueSummaryInformation issueSummary = IssueSummaryInformation(); 13 | } 14 | ///Returns the parameters for all data in the repo page 15 | abstract class DashboardServiceInterface { 16 | ///Returns the dashboard parameters for all data in repo page, 17 | ///currently contains dummy providers 18 | Future getDashboardParameters(); 19 | } 20 | 21 | ///Implements the dummy providers for the repo page 22 | class DashboardServiceImplementor extends DashboardServiceInterface{ 23 | ///Constructor 24 | DashboardServiceImplementor(this.issueProvider, this.pullServiceProvider); 25 | ///Instance of the issue provider 26 | IssueProviderInterface issueProvider; 27 | ///Instance of the pull request provider 28 | PullProviderInterface pullServiceProvider; 29 | 30 | @override 31 | Future getDashboardParameters() async{ 32 | final params = DashboardParameterList(); 33 | params.issueSummary = await issueProvider.getIssueSummaryInformation(); 34 | params.pullRequestSummary = 35 | await pullServiceProvider.getPullSummaryInformation(); 36 | return params; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/service/fire_base.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:admin_dashboard/dto/admin_dashboard_cache_model.dart'; 4 | import 'package:admin_dashboard/dto/constant.dart'; 5 | import 'package:admin_dashboard/dto/issue_model.dart'; 6 | import 'package:admin_dashboard/dto/pull_model.dart'; 7 | import 'package:admin_dashboard/dto/repo_model.dart'; 8 | import 'package:admin_dashboard/main.dart'; 9 | import 'package:admin_dashboard/service/basic_service.dart'; 10 | import 'package:firebase_core/firebase_core.dart'; 11 | import 'package:flutter/cupertino.dart'; 12 | import 'package:http/http.dart' as http; 13 | 14 | 15 | 16 | ///implements the methods that Firebase as a middleware 17 | ///will provide for GitHub 18 | class FireBaseService implements BasicServiceInterface { 19 | ///FirebaseService constructor that takes firebaseApp as a parameter 20 | FireBaseService(this.firebaseApp); 21 | ///Instance of firebaseApp 22 | FirebaseApp firebaseApp; 23 | 24 | @override 25 | Future addIssue(Issue issue) async { 26 | const siteUrl = 27 | '${Constants.repoLogLink}' 28 | '/${Constants.repoNameTest}' 29 | '/${Constants.issues}.json'; 30 | 31 | try { 32 | final urlChat = Uri.parse(siteUrl); 33 | final issue = Issue( 34 | issueID: 250, 35 | state: 'closed', 36 | title: 'Test Issue 25', 37 | loggedAt: DateTime.now(), 38 | closedAt: DateTime.now(), 39 | closedBy: 'Amer', 40 | commentsNumber: 4, 41 | repository: Constants.repoNameTest, 42 | ); 43 | debugPrint(issue.toJson().toString()); 44 | final response1 = await http.post(urlChat, 45 | body: json.encode(issue.toJson()),); 46 | debugPrint(response1.statusCode.toString()); 47 | return issue; 48 | } catch (error) { 49 | rethrow; 50 | } 51 | } 52 | 53 | @override 54 | List getAllIssues() { 55 | // TODO: implement getAllIssues 56 | throw UnimplementedError(); 57 | } 58 | 59 | @override 60 | Future> getAllRepoIssues( 61 | String repoName, 62 | BuildContext context, 63 | AdminDashboardCache cache, 64 | ) async { 65 | final result = []; 66 | final url = Uri.parse('https://api.github.com$repoName'); 67 | try { 68 | final response = await http.get( 69 | url, 70 | headers: { 71 | 'Authorization': 'Bearer ${EnvironmentConfig.token}', 72 | }, 73 | ); 74 | if (response.statusCode != 200) { 75 | return result; 76 | } 77 | 78 | final body = response.body; 79 | if (body == 'null') { 80 | return result; 81 | } 82 | 83 | final issues = json.decode(response.body); 84 | List issueList; 85 | issueList = issues as List; 86 | for (final element in issueList) { 87 | Map issue; 88 | issue = element as Map; 89 | debugPrint("id=${issue['id'] ?? "null"}"); 90 | debugPrint("title=${issue['title'] ?? "null"}"); 91 | } 92 | 93 | return result; 94 | } catch (error) { 95 | rethrow; 96 | } 97 | } 98 | 99 | @override 100 | Future addPull(Pull pr) async { 101 | const siteUrl = 102 | '${Constants.siteUrl}' 103 | '/${Constants.repoNameTest}' 104 | '/${Constants.pulls}.json'; 105 | try { 106 | final urlChat = Uri.parse(siteUrl); 107 | Pull pull; 108 | pull = Pull( 109 | userId: 250, 110 | state: 'closed', 111 | title: 'Test PR', 112 | loggedAt: DateTime.now(), 113 | closedAt: DateTime.now(), 114 | closedBy: 'Amer', 115 | commentsNumber: 4, 116 | repository: 'Flutter Test', 117 | ); 118 | debugPrint(pull.toJson().toString()); 119 | final response2 = 120 | await http.post(urlChat, body: json.encode(pull.toJson())); 121 | debugPrint(response2.statusCode.toString()); 122 | return pull; 123 | } catch (error) { 124 | rethrow; 125 | } 126 | } 127 | 128 | @override 129 | List getAllPulls() { 130 | // TODO: implement getAllPulls 131 | throw UnimplementedError(); 132 | } 133 | 134 | @override 135 | Future> getAllRepoPulls( 136 | String repoName, BuildContext context, AdminDashboardCache cache,) { 137 | throw UnimplementedError(); 138 | } 139 | 140 | @override 141 | Future> getAllRepos( 142 | BuildContext context, 143 | AdminDashboardCache cache,) async { 144 | final result = []; 145 | var page =0; 146 | while(true) { 147 | page++; 148 | final url = Uri.parse('${Constants.gitApi}/' 149 | '${Constants.orgs}/' 150 | '${Constants.flc}/' 151 | '${Constants.repos}?page=$page',); 152 | try { 153 | final response = await http.get( 154 | url, 155 | headers: { 156 | 'Authorization': 'Bearer ${EnvironmentConfig.token}', 157 | }, 158 | ); 159 | if (response.statusCode != 200) { 160 | debugPrint('status code = ${response.statusCode} ' 161 | ,); 162 | return result; 163 | } 164 | 165 | final body = response.body; 166 | if (body == 'null') { 167 | return result; 168 | } 169 | final repos = json.decode(response.body); 170 | List repoList; 171 | repoList = repos as List; 172 | if(repoList.isEmpty){ 173 | break;//breaks the while loop 174 | } 175 | for (final element in repoList) { 176 | Map repo; 177 | repo = element as Map; 178 | final name 179 | = (repo['name'] == null) ? '' : repo['name'].toString(); 180 | debugPrint('name=$name'); 181 | debugPrint("url=${repo['url'] ?? "url"}"); 182 | 183 | if (name.isNotEmpty) { 184 | final repoObj = SimpleRepo( 185 | name: Constants.fluttercommunityPath + name,); 186 | result.add(repoObj); 187 | } 188 | } 189 | } catch (error) { 190 | rethrow; 191 | } 192 | } 193 | return result; 194 | 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /lib/service/github_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:admin_dashboard/dto/constant.dart'; 3 | import 'package:admin_dashboard/dto/issue.dart'; 4 | import 'package:admin_dashboard/main.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:http/http.dart' as http; 7 | 8 | ///class that handles retrieving data from github API 9 | class GithubService{ 10 | 11 | ///Retrieves all issues in a repository 12 | Future> getAllIssuesOfRepo( 13 | String repoName, 14 | ) async { 15 | final result = []; 16 | final url = 17 | Uri.parse('${Constants.repoGitUrl}$repoName${Constants.repoGitIssue}'); 18 | try { 19 | final response = await http.get( 20 | url, 21 | headers: { 22 | 'Authorization': 'Bearer ${EnvironmentConfig.token}', 23 | }, 24 | ); 25 | if (response.statusCode != 200) { 26 | return result; 27 | } 28 | 29 | final body = response.body; 30 | if (body == 'null') { 31 | return result; 32 | } 33 | 34 | final issues = json.decode(response.body); 35 | List issueList; 36 | issueList = issues as List; 37 | SimpleIssue myIssue; 38 | for (final element in issueList) { 39 | Map issue; 40 | issue = element as Map; 41 | //debugPrint("id=${issue['id'] ?? "null"}"); 42 | debugPrint("title=${issue['title'] ?? "null"}"); 43 | debugPrint("url=${issue['url'] ?? "null"}"); 44 | debugPrint("state=${issue['state'] ?? "null"}"); 45 | debugPrint("comments=${issue['comments'] ?? "null"}"); 46 | debugPrint("created_at=${issue['created_at'] ?? "null"}"); 47 | debugPrint("updated_at=${issue['updated_at'] ?? "null"}"); 48 | 49 | 50 | 51 | 52 | myIssue = SimpleIssue(title: issue['title']!.toString(), 53 | url: issue['url']!.toString(), 54 | state: issue['state']!.toString(), 55 | comments: int.tryParse(issue['comments']!.toString()) ?? 0, 56 | createdAt: 57 | DateTime.tryParse(issue['created_at']!.toString()) ?? DateTime.now(), 58 | updatedAt: 59 | DateTime.tryParse(issue['updated_at']!.toString()) ?? DateTime.now(), 60 | ); 61 | 62 | debugPrint(myIssue.toString()); 63 | debugPrint('\n********************************\n'); 64 | result.add(myIssue); 65 | 66 | } 67 | //debugPrint(result.toString()); 68 | return result; 69 | } catch (error) { 70 | rethrow; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/service/issue_service.dart: -------------------------------------------------------------------------------- 1 | ///Information regarding issues that is displayed in repo dashboard 2 | abstract class IssueProviderInterface { 3 | ///Retrieves total issues in a repo 4 | Future getTotalIssue(); 5 | ///Retrieves total closed issues in a repo 6 | Future getTotalClosedIssue(); 7 | ///Retrieves number of labeled issues in a repo 8 | Future getTagNumberIssue(); 9 | ///Retrieves number of dormant issues in a repo 10 | Future getTotalDormantIssue(); 11 | ///Retrieves number of issues that have been recently opened 12 | Future getRecentIssue(); 13 | ///Retrieves number of issues older than 6 months 14 | Future getOldIssue(); 15 | ///Retrieves number of total issues 16 | Future totalIssueTitle(); 17 | ///Retrieves Dormant issues keyword 18 | Future dormantIssueTitle(); 19 | ///Retrieves issues with high activity keyword 20 | Future issuesHighActivity(); 21 | ///Retrieves issues with low activity keyword 22 | Future issuesLowActivity(); 23 | ///Retrieves issues grouped by label keyword 24 | Future issuesLabeled(); 25 | ///Retrieves issues older than 6 months keyword 26 | Future issuesOldTitle(); 27 | ///contains all the info regarding issues in the repo dashboard 28 | Future getIssueSummaryInformation(); 29 | 30 | } 31 | ///Information regarding issues that is displayed in repo dashboard 32 | class IssueSummaryInformation { 33 | ///number of total issues 34 | String totalIssue = '...'; 35 | ///number of closed issues 36 | String totalClosedIssue = '...'; 37 | ///number of dormant issues 38 | String totalDormantIssue = '...'; 39 | ///number of recent issues 40 | String recentIssue = '...'; 41 | ///number of old issues 42 | String oldIssue = '...'; 43 | ///Dormant issues keyword 44 | String dormantIssueTitle = '...'; 45 | ///Total issues keyword 46 | String totalIssueTitle = '...'; 47 | ///Issues with label keyword where the user can then see issues with 48 | ///certain labels 49 | String issuesLabeled = '...'; 50 | ///Issues with high activity where a user clicks and then sees a list of 51 | ///issues with high activity 52 | String issuesHighActivity = '...'; 53 | ///Issues with low activity where a user clicks and then sees a list of 54 | ///issues with low activity 55 | String issuesLowActivity = '...'; 56 | /// issues older than 6 months key word 57 | String issuesOldTitle = '...'; 58 | } 59 | 60 | ///Dummy issue provider that contains all hardcoded info until they are 61 | ///fully implemented 62 | class DummyIssueProvider extends IssueProviderInterface { 63 | @override 64 | Future getOldIssue() async { 65 | return 13; 66 | } 67 | 68 | @override 69 | Future getRecentIssue() async { 70 | return 2; 71 | } 72 | 73 | @override 74 | Future getTagNumberIssue() async { 75 | return 9; 76 | } 77 | 78 | @override 79 | Future getTotalClosedIssue() async { 80 | return 4; 81 | } 82 | 83 | @override 84 | Future getTotalDormantIssue() async { 85 | return 7; 86 | } 87 | 88 | @override 89 | Future getTotalIssue() async { 90 | return 5; 91 | } 92 | 93 | @override 94 | Future dormantIssueTitle() async { 95 | return 'Dormant Issues: '; 96 | } 97 | 98 | @override 99 | Future totalIssueTitle() async { 100 | return 'Total Issues: '; 101 | } 102 | 103 | @override 104 | Future issuesHighActivity() async { 105 | return 'Open issues with high activity '; 106 | } 107 | 108 | @override 109 | Future issuesLabeled() async { 110 | return 'Open issues grouped by label '; 111 | } 112 | 113 | @override 114 | Future issuesLowActivity() async { 115 | return 'Open issues with no activity'; 116 | 117 | } 118 | 119 | @override 120 | Future issuesOldTitle() async { 121 | return 'Open issues older than 6 months'; 122 | } 123 | 124 | @override 125 | Future getIssueSummaryInformation() async { 126 | ///info contains all the information regarding the issue dummy provider 127 | final info = IssueSummaryInformation(); 128 | info..oldIssue = (await getOldIssue()).toString() 129 | ..recentIssue = (await getRecentIssue()).toString() 130 | ..totalClosedIssue = (await getTotalClosedIssue()).toString() 131 | ..totalDormantIssue = (await getTotalDormantIssue()).toString() 132 | ..totalIssue = (await getTotalIssue()).toString() 133 | ..dormantIssueTitle = (await dormantIssueTitle()) 134 | ..totalIssueTitle = (await totalIssueTitle()) 135 | ..issuesHighActivity = (await issuesHighActivity()) 136 | ..issuesLabeled = (await issuesLabeled()) 137 | ..issuesLowActivity = (await issuesLowActivity()) 138 | ..issuesOldTitle = (await issuesOldTitle()); 139 | return info; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lib/service/pull_request_service.dart: -------------------------------------------------------------------------------- 1 | ///Information regarding PRs that is displayed in the repo dashboard 2 | abstract class PullProviderInterface { 3 | ///Retrieves number of total PRs 4 | Future getTotalPull(); 5 | ///Retrieves number of total closed PRs 6 | Future getTotalClosedPull(); 7 | ///Retrieves number of PRs that are dormant 8 | Future getTotalDormantPull(); 9 | ///Retrieves number of recent PRs 10 | Future getRecentPull(); 11 | ///Retrieves keyword PRs older than 6 months 12 | Future getOldPull(); 13 | ///Retrieves the title for total PRs 14 | Future totalPullTitle(); 15 | ///Retrieves the title for Dormant PRs 16 | Future dormantPullTitle(); 17 | ///Retrieves keyword PRs with high activity 18 | Future pullsHighActivity(); 19 | ///Retrieves keyword PRs with low activity 20 | Future pullsLowActivity(); 21 | ///Retrieves keyword PRs with labels 22 | Future pullsLabeled(); 23 | ///Retrieves keyword PRs that are older than 6 months 24 | Future pullsOldTitle(); 25 | ///Contains all the information regarding PRs in the repo dashboard 26 | Future getPullSummaryInformation(); 27 | } 28 | ///Information regarding PRs that is displayed in the repo dashboard 29 | class PullSummaryInformation { 30 | ///Total number of PRs in a repo 31 | String totalPull = '...'; 32 | ///Total number of closed PRs in a repo 33 | String totalClosedPull = '...'; 34 | ///Total number of dormant issues 35 | String totalDormantPull = '...'; 36 | ///Recent PRs 37 | String recentPull = '...'; 38 | ///Old PRs 39 | String oldPull = '...'; 40 | ///Title for dormant PRs 41 | String dormantPullTitle = '...'; 42 | ///Title for total PRs 43 | String totalIPullTitle = '...'; 44 | ///PRs grouped by label keyword 45 | String pullsLabeled = '...'; 46 | ///PRs with high activity keyword 47 | String pullsHighActivity = '...'; 48 | ///PRs with low activity keyword 49 | String pullsLowActivity = '...'; 50 | ///PRs older than 6 months keyword 51 | String pullsOldTitle = '...'; 52 | } 53 | ///Dummy pull request provider that contains everything hardcoded until it 54 | ///is fully implemented 55 | class DummyPullProvider extends PullProviderInterface { 56 | @override 57 | Future getOldPull() async { 58 | return 8; 59 | } 60 | 61 | @override 62 | Future getRecentPull() async { 63 | return 4; 64 | } 65 | 66 | 67 | @override 68 | Future getTotalClosedPull() async { 69 | return 12; 70 | } 71 | 72 | @override 73 | Future getTotalDormantPull() async { 74 | return 11; 75 | } 76 | 77 | @override 78 | Future getTotalPull() async { 79 | return 42; 80 | } 81 | 82 | @override 83 | Future dormantPullTitle() async { 84 | return 'Dormant Pull Requests: '; 85 | } 86 | 87 | @override 88 | Future totalPullTitle() async { 89 | return 'Total Pull Requests: '; 90 | } 91 | 92 | @override 93 | Future pullsHighActivity() async { 94 | return 'PRs with high activity'; 95 | 96 | } 97 | 98 | @override 99 | Future pullsLabeled() async { 100 | return 'PRs grouped by label'; 101 | 102 | } 103 | 104 | @override 105 | Future pullsLowActivity() async { 106 | return 'PRs with low activity'; 107 | 108 | } 109 | 110 | @override 111 | Future pullsOldTitle() async { 112 | return 'PRs older than 2 months'; 113 | 114 | } 115 | @override 116 | Future getPullSummaryInformation() async { 117 | final info = PullSummaryInformation(); 118 | info..oldPull = (await getOldPull()).toString() 119 | ..recentPull = (await getRecentPull()).toString() 120 | ..totalClosedPull = (await getTotalClosedPull()).toString() 121 | ..totalDormantPull = (await getTotalDormantPull()).toString() 122 | ..totalPull = (await getTotalPull()).toString() 123 | ..dormantPullTitle = (await dormantPullTitle()) 124 | ..totalIPullTitle = (await totalPullTitle()) 125 | ..pullsHighActivity = (await pullsHighActivity()) 126 | ..pullsLabeled = (await pullsLabeled()) 127 | ..pullsLowActivity = (await pullsLowActivity()) 128 | ..pullsOldTitle = (await pullsOldTitle()); 129 | 130 | return info; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/service/table_data_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/table_data.dart'; 2 | 3 | ///Provider for the issue table 4 | abstract class TableDataProviderInterface { 5 | ///Constructor 6 | TableDataProviderInterface(this.repoName); 7 | ///Name of the repository 8 | String repoName; 9 | ///Issues opened within the last 6 months 10 | Future getOpenInLastSixMonths(); 11 | ///To be implemented 12 | Future getSecondTable(); 13 | } 14 | 15 | ///Dummy table data provider 16 | class DummyTableDataProvider extends TableDataProviderInterface { 17 | ///Constructor 18 | DummyTableDataProvider(String repoName) : super(repoName); 19 | 20 | @override 21 | Future getOpenInLastSixMonths() async{ 22 | return TableData().add(TableDataRow().addValue('Name', 'Bug fix'). 23 | addValue('Open', '3 days'),) 24 | .add(TableDataRow().addValue('Name', 'Enhancement'). 25 | addValue('Open', '7 days'),) 26 | .add(TableDataRow().addValue('Name', 'Feature'). 27 | addValue('Open', '1 month'),) 28 | .add(TableDataRow().addValue('Name', 'Help wanted'). 29 | addValue('Open', '4 months'),); 30 | } 31 | 32 | @override 33 | Future getSecondTable() async{ 34 | return TableData().add(TableDataRow().addValue('Name', 'Bug fix'). 35 | addValue('Open', '3 days'),) 36 | .add(TableDataRow().addValue('Name', 'Enhancement'). 37 | addValue('Open', '7 days'),) 38 | .add(TableDataRow().addValue('Name', 'Feature'). 39 | addValue('Open', '1 month'),) 40 | .add(TableDataRow().addValue('Name', 'Help wanted'). 41 | addValue('Open', '4 months'),); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /lib/view_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:admin_dashboard/dto/constant.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// Utility class used to create and display dialogs 5 | class ViewUtil { 6 | 7 | /// set up the AlertDialog 8 | static void showMessage(String title, String message, BuildContext context) { 9 | final Widget okButton = TextButton( 10 | child: const Text(Constants.ok), 11 | onPressed: () { 12 | Navigator.pop(context); 13 | }, 14 | ); 15 | 16 | final alert = AlertDialog( 17 | title: Text(title), 18 | content: Text(message), 19 | actions: [ 20 | okButton, 21 | ], 22 | ); 23 | 24 | /// show the dialog 25 | showDialog( 26 | context: context, 27 | builder: (BuildContext context) { 28 | return alert; 29 | }, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: admin_dashboard 2 | description: Admin Dashboard to assist admins and maintainers by pinging them to repositories that require attention 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | version: 1.0.0+1 10 | 11 | environment: 12 | sdk: ">=2.16.2 <3.0.0" 13 | 14 | # Dependencies specify other packages that your package needs in order to work. 15 | dependencies: 16 | flutter: 17 | sdk: flutter 18 | 19 | build_runner: 20 | cupertino_icons: ^1.0.2 21 | # These 2 are use for mobile only 22 | firebase_auth: ^3.6.3 23 | firebase_core: ^1.20.1 24 | 25 | flutter_riverpod: ^1.0.0 26 | github_sign_in: ^0.0.5-dev.4 27 | http: ^0.13.4 28 | json_serializable: ^6.2.0 29 | json_annotation: 30 | easy_sidemenu: ^0.3.1 31 | flutter_dotenv: ^5.0.2 32 | fl_chart: ^0.55.1 33 | web_browser_detect: ^2.0.3 34 | # cloud_functions: ^3.3.2 35 | 36 | dev_dependencies: 37 | flutter_test: 38 | sdk: flutter 39 | injectable_generator: ^1.5.2 40 | 41 | flutter_lints: 42 | 43 | 44 | # The following section is specific to Flutter. 45 | flutter: 46 | assets: 47 | - env_var.env 48 | uses-material-design: true 49 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/admin_dashboard/3c89951dd97b1fd648b8bde754cb252dd2f604bb/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/admin_dashboard/3c89951dd97b1fd648b8bde754cb252dd2f604bb/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/admin_dashboard/3c89951dd97b1fd648b8bde754cb252dd2f604bb/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/admin_dashboard/3c89951dd97b1fd648b8bde754cb252dd2f604bb/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/admin_dashboard/3c89951dd97b1fd648b8bde754cb252dd2f604bb/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | admin_dashboard_v2 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 66 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin_dashboard_v2", 3 | "short_name": "admin_dashboard_v2", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | --------------------------------------------------------------------------------