Base pages implementation

This commit is contained in:
Igor Kulikov
2021-05-06 14:51:26 +03:00
parent 7bec80ef15
commit 64a7cdf167
80 changed files with 2878 additions and 380 deletions

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entity_details_page.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class AssetDetailsPage extends EntityDetailsPage<AssetInfo> {
AssetDetailsPage(TbContext tbContext, String assetId):
super(tbContext,
entityId: assetId,
defaultTitle: 'Asset');
@override
Future<AssetInfo> fetchEntity(String assetId) {
return tbClient.getAssetService().getAssetInfo(assetId);
}
@override
Widget buildEntityDetails(BuildContext context, AssetInfo asset) {
return ListTile(
title: Text('${asset.name}'),
subtitle: Text('${asset.type}'),
);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/modules/asset/assets_page.dart';
import 'asset_details_page.dart';
class AssetRoutes extends TbRoutes {
late var assetsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return AssetsPage(tbContext);
});
late var assetDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return AssetDetailsPage(tbContext, params["id"][0]);
});
AssetRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/assets", handler: assetsHandler);
router.define("/asset/:id", handler: assetDetailsHandler);
}
}

View File

@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
mixin AssetsBase on EntitiesBase<AssetInfo> {
@override
String get title => 'Assets';
@override
String get noItemsFoundText => 'No assets found';
@override
Future<PageData<AssetInfo>> fetchEntities(PageLink pageLink) {
if (tbClient.isTenantAdmin()) {
return tbClient.getAssetService().getTenantAssetInfos(pageLink);
} else {
return tbClient.getAssetService().getCustomerAssetInfos(tbClient.getAuthUser()!.customerId, pageLink);
}
}
@override
Widget buildEntityCard(BuildContext context, AssetInfo asset, bool briefView) {
return Row(
mainAxisSize: briefView ? MainAxisSize.min : MainAxisSize.max,
children: [
Flexible(
fit: briefView ? FlexFit.loose : FlexFit.tight,
child:
Container(
padding: EdgeInsets.symmetric(vertical: briefView ? 9 : 10, horizontal: 16),
child: Row(
mainAxisSize: briefView ? MainAxisSize.min : MainAxisSize.max,
children: [
Flexible(
fit: briefView ? FlexFit.loose : FlexFit.tight,
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${asset.name}',
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.7
))
),
Text('${asset.type}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
)
),
(!briefView ? Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(asset.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
) : Container())
],
),
)
)
]
);
}
@override
void onEntityDetails(AssetInfo asset) {
navigateTo('/asset/${asset.id!.id}');
}
}

View File

@@ -0,0 +1,16 @@
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_page.dart';
import 'package:thingsboard_app/modules/asset/assets_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class AssetsPage extends EntitiesPage<AssetInfo> with AssetsBase {
AssetsPage(TbContext tbContext) : super(tbContext);
@override
String get noMoreItemsText => 'No more assets';
@override
String get searchHint => 'Search assets';
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_widget.dart';
import 'package:thingsboard_app/modules/asset/assets_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class AssetsWidget extends EntitiesWidget<AssetInfo> with AssetsBase {
AssetsWidget(TbContext tbContext, {EntitiesWidgetController? controller}): super(tbContext, controller: controller);
@override
void onViewAll() {
navigateTo('/assets');
}
}

View File

@@ -0,0 +1,250 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:thingsboard_app/constants/api_path.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:url_launcher/url_launcher.dart';
typedef DashboardTitleCallback = void Function(String title);
class Dashboard extends TbContextWidget<Dashboard, _DashboardState> {
final String _dashboardId;
final String? _state;
final bool? _home;
final bool? _hideToolbar;
final bool _fullscreen;
final DashboardTitleCallback? _titleCallback;
Dashboard(TbContext tbContext, {required String dashboardId, required bool fullscreen,
DashboardTitleCallback? titleCallback, String? state, bool? home,
bool? hideToolbar}):
this._dashboardId = dashboardId,
this._fullscreen = fullscreen,
this._titleCallback = titleCallback,
this._state = state,
this._home = home,
this._hideToolbar = hideToolbar,
super(tbContext);
@override
_DashboardState createState() => _DashboardState();
}
class _DashboardState extends TbContextState<Dashboard, _DashboardState> {
final Completer<InAppWebViewController> _controller = Completer<InAppWebViewController>();
final ValueNotifier<bool> webViewLoading = ValueNotifier(true);
final GlobalKey webViewKey = GlobalKey();
InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
useShouldOverrideUrlLoading: true,
mediaPlaybackRequiresUserGesture: false,
javaScriptEnabled: true,
),
android: AndroidInAppWebViewOptions(
useHybridComposition: false,
thirdPartyCookiesEnabled: true
),
ios: IOSInAppWebViewOptions(
allowsInlineMediaPlayback: true,
));
late String _dashboardUrl;
late String _currentDashboardId;
late String? _currentDashboardState;
@override
void initState() {
super.initState();
_dashboardUrl = thingsBoardApiEndpoint + '/dashboard/' + widget._dashboardId;
List<String> params = [];
if (widget._state != null) {
params.add('state=${widget._state}');
}
if (widget._home == true) {
params.add('embedded=true');
}
if (widget._hideToolbar == true) {
params.add('hideToolbar=true');
}
if (params.isNotEmpty) {
_dashboardUrl += '?${params.join('&')}';
}
_currentDashboardId = widget._dashboardId;
_currentDashboardState = widget._state;
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
var controller = await _controller.future;
if (await controller.canGoBack()) {
await controller.goBack();
return false;
}
return true;
},
child: Stack(
children: [
InAppWebView(
key: webViewKey,
initialUrlRequest: URLRequest(url: Uri.parse(_dashboardUrl)),
initialOptions: options,
onWebViewCreated: (webViewController) {
webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardStateNameHandler", callback: (args) async {
log.debug("Invoked tbMobileDashboardStateNameHandler: $args");
webViewLoading.value = false;
if (args.isNotEmpty && args[0] is String) {
if (widget._titleCallback != null) {
widget._titleCallback!(args[0]);
}
}
});
webViewController.addJavaScriptHandler(handlerName: "tbMobileHandler", callback: (args) async {
log.debug("Invoked tbMobileHandler: $args");
return await widgetActionHandler.handleWidgetMobileAction(args, webViewController);
});
_controller.complete(webViewController);
},
shouldOverrideUrlLoading: (controller, navigationAction) async {
var uri = navigationAction.request.url!;
var uriString = uri.toString();
log.debug('disallowing navigation to $uriString');
if (await canLaunch(uriString)) {
await launch(uriString);
}
return NavigationActionPolicy.CANCEL;
},
onUpdateVisitedHistory: (controller, url, androidIsReload) async {
if (url != null) {
String newStateId = url.pathSegments.last;
log.debug('onUpdateVisitedHistory: $newStateId');
if (newStateId == 'profile') {
webViewLoading.value = true;
await controller.goBack();
await navigateTo('/profile');
webViewLoading.value = false;
return;
} else if (newStateId == 'login') {
webViewLoading.value = true;
await controller.pauseTimers();
await controller.stopLoading();
await tbClient.logout();
return;
} else if (['devices', 'assets', 'dashboards'].contains(newStateId)) {
var controller = await _controller.future;
await controller.goBack();
navigateTo('/$newStateId');
return;
} else {
if (url.pathSegments.length > 1) {
var segmentName = url.pathSegments[url.pathSegments.length-2];
if (segmentName == 'dashboards' && widget._home != true) {
webViewLoading.value = true;
var targetPath = _createDashboardNavigationPath(newStateId, fullscreen: widget._fullscreen);
await navigateTo(targetPath, replace: true);
return;
} else if (segmentName == 'dashboard') {
_currentDashboardId = newStateId;
_currentDashboardState = url.queryParameters['state'];
return;
}
}
webViewLoading.value = true;
if (widget._home == true) {
await navigateTo('/home', replace: true);
} else {
var targetPath = _createDashboardNavigationPath(_currentDashboardId, state: _currentDashboardState, fullscreen: widget._fullscreen);
await navigateTo(targetPath, replace: true);
}
}
}
},
onConsoleMessage: (controller, consoleMessage) {
log.debug('[JavaScript console] ${consoleMessage.messageLevel}: ${consoleMessage.message}');
},
onLoadStart: (controller, url) async {
log.debug('onLoadStart: $url');
await _setTokens(controller.webStorage.localStorage);
},
onLoadStop: (controller, url) {
},
),
ValueListenableBuilder(
valueListenable: webViewLoading,
builder: (BuildContext context, bool loading, child) {
if (!loading) {
return SizedBox.shrink();
} else {
return Container(
decoration: BoxDecoration(color: Colors.white),
child: Center(
child: CircularProgressIndicator()
),
);
}
}
)
],
)
);
}
String _createDashboardNavigationPath(String dashboardId, {bool? fullscreen, String? state}) {
var targetPath = '/dashboard/$dashboardId';
List<String> params = [];
if (state != null) {
params.add('state=$state');
}
if (fullscreen != null) {
params.add('fullscreen=$fullscreen');
}
if (params.isNotEmpty) {
targetPath += '?${params.join('&')}';
}
return targetPath;
}
Future<void> _setTokens(Storage storage) async {
String jwtToken = tbClient.getJwtToken()!;
int jwtTokenExpiration = _getClientExpiration(jwtToken);
String refreshToken = tbClient.getRefreshToken()!;
int refreshTokenExpiration = _getClientExpiration(refreshToken);
await storage.setItem(key: 'jwt_token', value: jwtToken);
await storage.setItem(key: 'jwt_token_expiration', value: jwtTokenExpiration);
await storage.setItem(key: 'refresh_token', value: refreshToken);
await storage.setItem(key: 'refresh_token_expiration', value: refreshTokenExpiration);
}
/* String _setTokensJavaScript() {
String jwtToken = tbClient.getJwtToken()!;
int jwtTokenExpiration = _getClientExpiration(jwtToken);
String refreshToken = tbClient.getRefreshToken()!;
int refreshTokenExpiration = _getClientExpiration(refreshToken);
return "window.localStorage.setItem('jwt_token','$jwtToken');\n"+
"window.localStorage.setItem('jwt_token_expiration','$jwtTokenExpiration');\n"+
"window.localStorage.setItem('refresh_token','$refreshToken');\n"+
"window.localStorage.setItem('refresh_token_expiration','$refreshTokenExpiration');";
} */
int _getClientExpiration(String token) {
var decodedToken = JwtDecoder.decode(tbClient.getJwtToken()!);
int issuedAt = decodedToken['iat'];
int expTime = decodedToken['exp'];
int ttl = expTime - issuedAt;
int clientExpiration = DateTime.now().millisecondsSinceEpoch + ttl * 1000;
return clientExpiration;
}
}

View File

@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/modules/dashboard/dashboard.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
class DashboardPage extends TbPageWidget<DashboardPage, _DashboardPageState> {
final String? _dashboardTitle;
final String _dashboardId;
final String? _state;
final bool _fullscreen;
DashboardPage(TbContext tbContext, {required String dashboardId, required bool fullscreen, String? dashboardTitle, String? state}):
_dashboardId = dashboardId,
_fullscreen = fullscreen,
_dashboardTitle = dashboardTitle,
_state = state,
super(tbContext);
@override
_DashboardPageState createState() => _DashboardPageState();
}
class _DashboardPageState extends TbPageState<DashboardPage, _DashboardPageState> {
late ValueNotifier<String> dashboardTitleValue;
@override
void initState() {
super.initState();
dashboardTitleValue = ValueNotifier(widget._dashboardTitle ?? 'Dashboard');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: TbAppBar(
tbContext,
showLoadingIndicator: false,
elevation: 0,
title: ValueListenableBuilder<String>(
valueListenable: dashboardTitleValue,
builder: (context, title, widget) {
return FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text(title)
);
},
),
),
body: Dashboard(tbContext, dashboardId: widget._dashboardId, state: widget._state,
fullscreen: widget._fullscreen, titleCallback: (title) {
dashboardTitleValue.value = title;
}),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/modules/main/main_page.dart';
import 'dashboard_page.dart';
class DashboardRoutes extends TbRoutes {
late var dashboardsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return MainPage(tbContext, path: '/dashboards');
});
late var dashboardDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
var fullscreen = params['fullscreen']?.first == 'true';
var dashboardTitle = params['title']?.first;
var state = params['state']?.first;
return DashboardPage(tbContext, dashboardId: params["id"]![0], fullscreen: fullscreen,
dashboardTitle: dashboardTitle, state: state);
});
DashboardRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/dashboards", handler: dashboardsHandler);
router.define("/dashboard/:id", handler: dashboardDetailsHandler);
}
}

View File

@@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
mixin DashboardsBase on EntitiesBase<DashboardInfo> {
@override
String get title => 'Dashboards';
@override
String get noItemsFoundText => 'No dashboards found';
@override
Future<PageData<DashboardInfo>> fetchEntities(PageLink pageLink) {
if (tbClient.isTenantAdmin()) {
return tbClient.getDashboardService().getTenantDashboards(pageLink);
} else {
return tbClient.getDashboardService().getCustomerDashboards(tbClient.getAuthUser()!.customerId, pageLink);
}
}
@override
Widget buildEntityCard(BuildContext context, DashboardInfo dashboard, bool briefView) {
return Row(
mainAxisSize: briefView ? MainAxisSize.min : MainAxisSize.max,
children: [
Flexible(
fit: briefView ? FlexFit.loose : FlexFit.tight,
child:
Container(
padding: EdgeInsets.symmetric(vertical: briefView ? 9 : 10, horizontal: 16),
child: Row(
mainAxisSize: briefView ? MainAxisSize.min : MainAxisSize.max,
children: [
Flexible(
fit: briefView ? FlexFit.loose : FlexFit.tight,
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${dashboard.title}',
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.7
))
),
Text('${_dashboardDetailsText(dashboard)}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
)
),
(!briefView ? Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(dashboard.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
) : Container())
],
),
)
)
]
);
}
String _dashboardDetailsText(DashboardInfo dashboard) {
if (tbClient.isTenantAdmin()) {
if (_isPublicDashboard(dashboard)) {
return 'Public';
} else {
return dashboard.assignedCustomers.map((e) => e.title).join(', ');
}
}
return '';
}
bool _isPublicDashboard(DashboardInfo dashboard) {
return dashboard.assignedCustomers.any((element) => element.isPublic);
}
@override
void onEntityDetails(DashboardInfo dashboard) {
navigateTo('/dashboard/${dashboard.id!.id}?title=${dashboard.title}');
}
}

View File

@@ -0,0 +1,18 @@
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_page.dart';
import 'package:thingsboard_app/modules/dashboard/dashboards_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class DashboardsPage extends EntitiesPage<DashboardInfo> with DashboardsBase {
DashboardsPage(TbContext tbContext) :
super(tbContext);
@override
String get noMoreItemsText => 'No more dashboards';
@override
String get searchHint => 'Search dashboards';
}

View File

@@ -0,0 +1,15 @@
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_widget.dart';
import 'package:thingsboard_app/modules/dashboard/dashboards_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class DashboardsWidget extends EntitiesWidget<DashboardInfo> with DashboardsBase {
DashboardsWidget(TbContext tbContext, {EntitiesWidgetController? controller}): super(tbContext, controller: controller);
@override
void onViewAll() {
navigateTo('/dashboards');
}
}

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entity_details_page.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class DeviceDetailsPage extends EntityDetailsPage<DeviceInfo> {
DeviceDetailsPage(TbContext tbContext, String deviceId):
super(tbContext,
entityId: deviceId,
defaultTitle: 'Device');
@override
Future<DeviceInfo> fetchEntity(String deviceId) {
return tbClient.getDeviceService().getDeviceInfo(deviceId);
}
@override
Widget buildEntityDetails(BuildContext context, DeviceInfo device) {
return ListTile(
title: Text('${device.name}'),
subtitle: Text('${device.type}'),
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/modules/main/main_page.dart';
import 'device_details_page.dart';
import 'devices_page.dart';
class DeviceRoutes extends TbRoutes {
late var devicesHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return MainPage(tbContext, path: '/devices');
});
late var deviceDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return DeviceDetailsPage(tbContext, params["id"][0]);
});
DeviceRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/devices", handler: devicesHandler);
router.define("/device/:id", handler: deviceDetailsHandler);
}
}

View File

@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
mixin DevicesBase on EntitiesBase<DeviceInfo> {
@override
String get title => 'Devices';
@override
String get noItemsFoundText => 'No devices found';
@override
Future<PageData<DeviceInfo>> fetchEntities(PageLink pageLink) {
if (tbClient.isTenantAdmin()) {
return tbClient.getDeviceService().getTenantDeviceInfos(pageLink);
} else {
return tbClient.getDeviceService().getCustomerDeviceInfos(tbClient.getAuthUser()!.customerId, pageLink);
}
}
@override
void onEntityDetails(DeviceInfo device) {
navigateTo('/device/${device.id!.id}');
}
@override
Widget buildEntityCard(BuildContext context, DeviceInfo device, bool briefView) {
return Row(
mainAxisSize: briefView ? MainAxisSize.min : MainAxisSize.max,
children: [
Container(
width: briefView ? 58 : 60,
decoration: BoxDecoration(
color: Color(0xFFEEEEEE),
borderRadius: BorderRadius.horizontal(left: Radius.circular(briefView ? 4 : 6))
),
child: Center(
child: Icon(Icons.devices_other, color: Color(0xFFC2C2C2))
),
),
Flexible(
fit: briefView ? FlexFit.loose : FlexFit.tight,
child:
Container(
padding: EdgeInsets.symmetric(vertical: briefView ? 9 : 10, horizontal: 16),
child: Row(
mainAxisSize: briefView ? MainAxisSize.min : MainAxisSize.max,
children: [
Flexible(
fit: briefView ? FlexFit.loose : FlexFit.tight,
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${device.name}',
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.7
))
),
Text('${device.type}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
)
),
(!briefView ? Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(device.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
) : Container())
],
),
)
)
]
);
}
}

View File

@@ -1,209 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_page.dart';
import 'package:thingsboard_app/modules/device/devices_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
class DeviceInfoCard extends StatelessWidget {
final DeviceInfo device;
final void Function(DeviceInfo device)? onDetails;
DeviceInfoCard(this.device, {this.onDetails});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 5.0),
child: ListTile(
title: Text('${device.name}'),
subtitle: Text('${device.type}'),
trailing: IconButton(
icon: Icon(Icons.navigate_next),
onPressed: () {
if (onDetails != null) {
onDetails!(device);
}
},
),
)
)
);
}
}
class FirstPageExceptionIndicator extends StatelessWidget {
const FirstPageExceptionIndicator({
required this.title,
this.message,
this.onTryAgain,
Key? key,
}) : super(key: key);
final String title;
final String? message;
final VoidCallback? onTryAgain;
@override
Widget build(BuildContext context) {
final message = this.message;
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
child: Column(
children: [
Text(
title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline6,
),
if (message != null)
const SizedBox(
height: 16,
),
if (message != null)
Text(
message,
textAlign: TextAlign.center,
),
if (onTryAgain != null)
const SizedBox(
height: 48,
),
if (onTryAgain != null)
SizedBox(
height: 50,
width: double.infinity,
child: ElevatedButton.icon(
onPressed: onTryAgain,
icon: const Icon(
Icons.refresh,
color: Colors.white,
),
label: const Text(
'Try Again',
style: TextStyle(
fontSize: 16,
color: Colors.white,
),
),
),
),
],
),
),
);
}
}
class DevicesPage extends TbPageWidget<DevicesPage, _DevicesPageState> {
class DevicesPage extends EntitiesPage<DeviceInfo> with DevicesBase {
DevicesPage(TbContext tbContext) : super(tbContext);
@override
_DevicesPageState createState() => _DevicesPageState();
String get noMoreItemsText => 'No more devices';
@override
String get searchHint => 'Search devices';
}
class _DevicesPageState extends TbPageState<DevicesPage, _DevicesPageState> {
final _searchModeNotifier = ValueNotifier<bool>(false);
final PagingController<PageLink, DeviceInfo> _pagingController = PagingController(firstPageKey: PageLink(10));
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
bool dataLoading = false;
bool scheduleRefresh = false;
void refresh() {
if (dataLoading) {
scheduleRefresh = true;
} else {
_pagingController.refresh();
}
}
Future<void> _fetchPage(PageLink pageKey) async {
dataLoading = true;
try {
hideNotification();
final pageData = await tbContext.tbClient.getDeviceService().getTenantDeviceInfos(pageKey);
final isLastPage = !pageData.hasNext;
if (isLastPage) {
_pagingController.appendLastPage(pageData.data);
} else {
final nextPageKey = pageKey.nextPageLink();
_pagingController.appendPage(pageData.data, nextPageKey);
}
} catch (error) {
_pagingController.error = error;
} finally {
dataLoading = false;
if (scheduleRefresh) {
scheduleRefresh = false;
_pagingController.refresh();
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: TbAppBar(
tbContext,
title: const Text('Devices'),
searchModeNotifier: _searchModeNotifier,
searchHint: 'Search devices',
onSearch: (String searchText) {
_pagingController.firstPageKey.textSearch = searchText;
refresh();
},
),
body: Builder(
builder: (BuildContext context) {
return PagedListView(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<DeviceInfo>(
itemBuilder: (context, item, index) {
return DeviceInfoCard(
item,
onDetails: (device) {
print('open details: $device');
},
);
},
noMoreItemsIndicatorBuilder: (context) => FirstPageExceptionIndicator(
title: 'No more devices'
),
noItemsFoundIndicatorBuilder: (context) => FirstPageExceptionIndicator(
title: 'No devices found',
message: 'The list is currently empty.',
onTryAgain: () => refresh(),
)
),
);
}
),
bottomNavigationBar: BottomAppBar(
/* bottomNavigationBar: BottomAppBar(
shape: CircularNotchedRectangle(),
notchMargin: 4.0,
child: new Row(
@@ -222,48 +34,6 @@ class _DevicesPageState extends TbPageState<DevicesPage, _DevicesPageState> {
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add), onPressed: () {},),
/* SpeedDial(
animatedIcon: AnimatedIcons.menu_close,
animatedIconTheme: IconThemeData(size: 22),
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Colors.white,
visible: true,
curve: Curves.bounceIn,
children: [
// FAB 1
SpeedDialChild(
child: Icon(Icons.refresh),
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Colors.white,
onTap: () {
refresh();
/* setState(() {
var rng = Random();
var pageSize = 1 + rng.nextInt(9);
futureDevices = tbContext.tbClient.getDeviceService().getTenantDeviceInfos(PageLink(pageSize));
}); */
},
label: 'Refresh',
labelStyle: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16.0),
),
// FAB 2
SpeedDialChild(
child: Icon(Icons.logout),
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Colors.white,
onTap: () {
tbContext.tbClient.logout(requestConfig: RequestConfig(ignoreErrors: true));
},
label: 'Logout',
labelStyle: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16.0),
)
],
)*/
);
}
@@ -316,7 +86,7 @@ class _DevicesPageState extends TbPageState<DevicesPage, _DevicesPageState> {
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Colors.white,
onTap: () {
tbContext.tbClient.logout(requestConfig: RequestConfig(ignoreErrors: true));
tbClient.logout(requestConfig: RequestConfig(ignoreErrors: true));
},
label: 'Logout',
labelStyle: TextStyle(
@@ -326,3 +96,4 @@ class _DevicesPageState extends TbPageState<DevicesPage, _DevicesPageState> {
],
);
}
*/

View File

@@ -0,0 +1,15 @@
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_widget.dart';
import 'package:thingsboard_app/modules/device/devices_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class DevicesWidget extends EntitiesWidget<DeviceInfo> with DevicesBase {
DevicesWidget(TbContext tbContext, {EntitiesWidgetController? controller}): super(tbContext, controller: controller);
@override
void onViewAll() {
navigateTo('/devices');
}
}

View File

@@ -1,11 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/entity/entities_widget.dart';
import 'package:thingsboard_app/modules/asset/assets_widget.dart';
import 'package:thingsboard_app/modules/dashboard/dashboards_widget.dart';
import 'package:thingsboard_app/modules/device/devices_widget.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
import 'package:thingsboard_app/modules/dashboard/dashboard.dart' as dashboardUi;
class HomePage extends TbPageWidget<HomePage, _HomePageState> {
class HomePage extends TbContextWidget<HomePage, _HomePageState> {
HomePage(TbContext tbContext) : super(tbContext);
@@ -14,35 +20,85 @@ class HomePage extends TbPageWidget<HomePage, _HomePageState> {
}
class _HomePageState extends TbPageState<HomePage, _HomePageState> {
class _HomePageState extends TbContextState<HomePage, _HomePageState> {
final EntitiesWidgetController _entitiesWidgetController = EntitiesWidgetController();
@override
void initState() {
super.initState();
}
@override
void dispose() {
_entitiesWidgetController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var homeDashboard = tbContext.homeDashboard;
var dashboardState = homeDashboard != null;
return Scaffold(
appBar: TbAppBar(
tbContext,
title: const Text('ThingsBoard'),
showLoadingIndicator: !dashboardState,
elevation: dashboardState ? 0 : null,
title: const Text('Home'),
),
body: Builder(
builder: (BuildContext context) {
return Center(child:
Column(
children: [
ElevatedButton(
child: Text('Devices'),
onPressed: () {
navigateTo('/devices');
},
)
],
)
);
}),
builder: (context) {
if (dashboardState) {
return _buildDashboardHome(context, homeDashboard!);
} else {
return _buildDefaultHome(context);
}
}
),
);
}
Widget _buildDashboardHome(BuildContext context, HomeDashboardInfo dashboard) {
return dashboardUi.Dashboard(tbContext, dashboardId: dashboard.dashboardId!.id!,
fullscreen: false, home: true, hideToolbar: dashboard.hideDashboardToolbar);
}
Widget _buildDefaultHome(BuildContext context) {
return RefreshIndicator(
onRefresh: () => _entitiesWidgetController.refresh(),
child: ListView(
children: _buildUserHome(context)
)
);
}
List<Widget> _buildUserHome(BuildContext context) {
if (tbClient.isSystemAdmin()) {
return _buildSysAdminHome(context);
} else if (tbClient.isTenantAdmin()) {
return _buildTenantAdminHome(context);
} else {
return _buildCustomerUserHome(context);
}
}
List<Widget> _buildSysAdminHome(BuildContext context) {
return [Container(child: Text('TODO: Implement'))];
}
List<Widget> _buildTenantAdminHome(BuildContext context) {
return [
AssetsWidget(tbContext, controller: _entitiesWidgetController),
DevicesWidget(tbContext, controller: _entitiesWidgetController),
DashboardsWidget(tbContext, controller: _entitiesWidgetController)
];
}
List<Widget> _buildCustomerUserHome(BuildContext context) {
return [
AssetsWidget(tbContext, controller: _entitiesWidgetController),
DevicesWidget(tbContext, controller: _entitiesWidgetController),
DashboardsWidget(tbContext, controller: _entitiesWidgetController)
];
}
}

View File

@@ -0,0 +1,20 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/modules/main/main_page.dart';
class HomeRoutes extends TbRoutes {
late var homeHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return MainPage(tbContext, path: '/home');
});
HomeRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/home", handler: homeHandler);
}
}

View File

@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/modules/dashboard/dashboards_page.dart';
import 'package:thingsboard_app/modules/device/devices_page.dart';
import 'package:thingsboard_app/modules/home/home_page.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class TbMainNavigationItem {
final Widget page;
final String title;
final Icon icon;
final String path;
TbMainNavigationItem({
required this.page,
required this.title,
required this.icon,
required this.path
});
static Map<Authority, Set<String>> mainPageStateMap = {
Authority.SYS_ADMIN: Set.unmodifiable(['/home', '/tenants', '/more']),
Authority.TENANT_ADMIN: Set.unmodifiable(['/home', '/devices', '/dashboards', '/more']),
Authority.CUSTOMER_USER: Set.unmodifiable(['/home', '/devices', '/dashboards', '/more']),
};
static bool isMainPageState(TbContext tbContext, String path) {
if (tbContext.isAuthenticated) {
return mainPageStateMap[tbContext.tbClient.getAuthUser()!.authority]!
.contains(path);
} else {
return false;
}
}
static List<TbMainNavigationItem> getItems(TbContext tbContext) {
if (tbContext.isAuthenticated) {
List<TbMainNavigationItem> items = [
TbMainNavigationItem(
page: HomePage(tbContext),
title: 'Home',
icon: Icon(Icons.home),
path: '/home'
)
];
switch(tbContext.tbClient.getAuthUser()!.authority) {
case Authority.SYS_ADMIN:
items.add(TbMainNavigationItem(
page: Scaffold(body: Center(child: Text('Tenants TODO'))),
title: 'Tenants',
icon: Icon(Icons.supervisor_account),
path: '/tenants'
));
break;
case Authority.TENANT_ADMIN:
case Authority.CUSTOMER_USER:
items.addAll([
TbMainNavigationItem(
page: DevicesPage(tbContext),
title: 'Devices',
icon: Icon(Icons.devices_other),
path: '/devices'
),
TbMainNavigationItem(
page: DashboardsPage(tbContext),
title: 'Dashboards',
icon: Icon(Icons.dashboard),
path: '/dashboards'
)
]);
break;
case Authority.REFRESH_TOKEN:
break;
case Authority.ANONYMOUS:
break;
}
items.add(TbMainNavigationItem(
page: Scaffold(body: Center(child: Text('TODO'))),
title: 'More',
icon: Icon(Icons.menu),
path: '/more'
));
return items;
} else {
return [];
}
}
}
class MainPage extends TbPageWidget<MainPage, _MainPageState> {
final String _path;
MainPage(TbContext tbContext, {required String path}):
_path = path, super(tbContext) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: Theme.of(tbContext.currentState!.context).colorScheme.primary,
systemNavigationBarIconBrightness: Brightness.dark
));
}
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends TbPageState<MainPage, _MainPageState> with TbMainState {
late int _currentIndex;
late final List<TbMainNavigationItem> _tabItems;
@override
void initState() {
super.initState();
_tabItems = TbMainNavigationItem.getItems(tbContext);
_currentIndex = _indexFromPath(widget._path);
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (_currentIndex > 0) {
setState(() => _currentIndex = 0);
return false;
}
return true;
},
child: Scaffold(
/* body: IndexedStack(
index: _currentIndex,
children: _tabItems.map((item) => item.page).toList(),
),*/
body: _tabItems.elementAt(_currentIndex).page,
bottomNavigationBar: Theme(
data: Theme.of(context).copyWith(
canvasColor: Theme.of(context).colorScheme.primary
),
child: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.white,
unselectedItemColor: Colors.white.withAlpha(97),
currentIndex: _currentIndex,
onTap: (int index) => setState(() => _currentIndex = index),
items: _tabItems.map((item) => BottomNavigationBarItem(
icon: item.icon,
label: item.title
)).toList()
)
)
),
);
}
int _indexFromPath(String path) {
return _tabItems.indexWhere((item) => item.path == path);
}
@override
bool canNavigate(String path) {
return _indexFromPath(path) > -1;
}
@override
navigateToPath(String path) {
int targetIndex = _indexFromPath(path);
if (_currentIndex != targetIndex) {
setState(() => _currentIndex = targetIndex);
}
}
}

View File

@@ -4,6 +4,7 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class ProfilePage extends TbPageWidget<ProfilePage, _ProfilePageState> {
@@ -16,9 +17,12 @@ class ProfilePage extends TbPageWidget<ProfilePage, _ProfilePageState> {
class _ProfilePageState extends TbPageState<ProfilePage, _ProfilePageState> {
late Future<User> userFuture;
@override
void initState() {
super.initState();
userFuture = tbClient.getUserService().getUser(tbClient.getAuthUser()!.userId!);
}
@override
@@ -30,10 +34,20 @@ class _ProfilePageState extends TbPageState<ProfilePage, _ProfilePageState> {
showProfile: false,
showLogout: true,
),
body: Builder(
builder: (BuildContext context) {
return Center(child: const Text('TODO: Implement!'));
}),
body: FutureBuilder<User>(
future: userFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
var user = snapshot.data!;
return ListTile(
title: Text('${user.email}'),
subtitle: Text('${user.firstName} ${user.lastName}'),
);
} else {
return Center(child: CircularProgressIndicator());
}
},
)
);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'profile_page.dart';
class ProfileRoutes extends TbRoutes {
late var profileHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return ProfilePage(tbContext);
});
ProfileRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/profile", handler: profileHandler);
}
}